Add RandomUniform-28 with deterministic generator attribute (Philox4x32-10) - #3
Open
strimo378 wants to merge 8 commits into
Open
Add RandomUniform-28 with deterministic generator attribute (Philox4x32-10)#3strimo378 wants to merge 8 commits into
strimo378 wants to merge 8 commits into
Conversation
…bute Adds an optional 'generator' string attribute to RandomUniform (new opset-28 schema) so the operator can be made deterministic and testable: - generator="unspecified" (default): implementation-defined PRNG, exactly the previous behavior, with no determinism guarantee. - generator="philox4x32_10": fully specified Philox-4x32-10 counter-based generator (Salmon et al., SC'11) keyed with the 64-bit seed. Element i depends only on (seed, i), so outputs are order-independent and parallelizable. Values are produced in the target data type: one 32-bit word per element for bfloat16/float16/float, two words (res53) for double, with low + r * (high - low) evaluated in dtype under IEEE 754 round-to-nearest-even. seed is required in this mode (enforced by shape inference). The v22 schema is preserved in old.cc. The version converter upgrades 27->28 compatibly and downgrades 28->27 only for generator="unspecified" (attribute dropped). The reference implementation gains a vectorized _Philox4x32 verified against the Random123 known-answer vectors. First-ever backend node tests for RandomUniform (4 cases) generate expected outputs from an independent inline Philox implementation, cross-checking the reference runtime bit-exactly. Docs and backend test data regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
The reference implementation used np.finfo to derive the significand
precision, which rejects ml_dtypes.bfloat16 ('data type not inexact');
use ml_dtypes.finfo, which covers native and ml_dtypes float types
alike. Adds a bfloat16 node test (expected outputs from the independent
inline Philox implementation) and a reference-evaluator test with
values hard-coded from the canonical Random123 implementation. The
node test is excluded on NumPy < 2.0, matching the existing bfloat16
exclusions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp
Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
- test_randomuniform_philox_multi_block: 35 elements span nine counter blocks with a partially consumed last block, stressing block increments, word ordering, and padding handling. - test_randomuniform_philox_nd_shape: 4-D shape with a singleton dimension and negative low, checking rank-independent row-major ordering and sign handling. (A dynamic output shape is not expressible for RandomUniform: shape is a required attribute and the op has no inputs; data-dependent shapes are the domain of RandomUniformLike.) - Reference-evaluator element-independence test: the row-major values of a smaller tensor must be a prefix of any larger tensor with the same seed across block boundaries, and a different seed must change the stream. - Every node test case now documents its intent in a docstring, which is published verbatim in docs/TestCoverage.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
A model run is a pure function of its inputs, so a hidden reset-vs-continue flag cannot exist in ONNX; following the explicit state pattern of CausalConvWithState (past_state/present_state) and Attention (KV cache), streaming becomes a wiring decision instead: - optional int64 scalar input 'offset' (default 0) occupies Philox counter words c2/c3 (two's complement bits read as unsigned), so the streams of different offsets never overlap, regardless of the output size. Element i is a pure function of (seed, offset, i). - optional int64 scalar output 'next_offset' = offset + 1 (wrapping on unsigned 64-bit overflow). Feeding it back as the next run's offset draws fresh, yet reproducible, values per run; feeding a constant (or omitting the input) reproduces the same values, which keeps the operator testable. The 28->27 downgrade adapter rejects nodes using the new input or output. Adds a node test with an input .pb for the offset and both outputs, an evaluator test chaining two runs through next_offset (disjoint streams, per-run replayability, default==offset 0), and shape inference and version converter tests. Docs and backend test data regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
next_offset carried no information: offset + 1 is trivially computable by the host, by an Add node in the graph, or as a loop-carried update — unlike present_state/KV-cache outputs, which cannot be derived without recomputation. Dropping it also removes an over-specification: since every offset value selects an independent stream, the operator need not prescribe any particular increment protocol; any non-repeating scheme (step counter, batch number) works. The offset input and its counter mapping are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
The legacy seed attribute is float32, whose 24-bit significand can only represent integers exactly up to 2^24 and cannot address the 64-bit Philox key space. Following the Constant value_int/value_float pattern (ONNX attributes have exactly one fixed type), a separate INT attribute seed_int64 now carries the key: its two's complement bits are the unsigned 64-bit Philox key (key0 = low word, key1 = high word). With generator="philox4x32_10", seed_int64 is required and the float seed must not be set (no fallback; enforced by shape inference). With generator="unspecified", both seeds remain implementation-defined as before. The 28->27 downgrade adapter rejects seed_int64. All philox tests and test data switched to seed_int64; expected values are unchanged since the numeric key values are the same. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
…, shared helpers Correctness: - The 28->27 adapter no longer rejects checker-valid models whose optional offset input is spelled as an empty string: the kUndefined placeholder the proto importer materializes is now detected and removed (precedent: axis_input_to_attribute.h), with a regression test. Real offset inputs are still rejected. - Shape inference now enforces that offset is a scalar via checkInputRank, as the spec promises; previously a [2,3] offset passed full_check and crashed the reference with a cryptic numpy error. - The reference implementation now enforces all three validation rules of the spec: a float seed alongside generator="philox4x32_10" raises instead of being silently ignored. Rollout preparation (the same mechanism is planned for five more ops): - generator/seed/seed_int64/offset attribute docs and the validation logic moved to shared constants and ValidateRandomGeneratorAttributes() in onnx/defs/generator/utils. - The downgrade adapter is parametrized (op name, number of legacy inputs) and renamed to RandomGenerator_28_27, so the Like-ops with a mandatory first input can reuse it. - _deterministic_uniform now owns validation, offset coercion, and the bit-exact affine step low + r * (high - low) in the target dtype, so future ops cannot diverge in the last ULP; misleading error text fixed. Performance (verified bit-identical, test data unchanged): - offset counter words broadcast as scalars instead of np.full - float32 intermediate for non-double dtypes - astype(dtype, copy=False) avoids a full copy in the double path - block-to-word-stream scaffolding deduplicated (_words helper) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
strimo378
force-pushed
the
claude/operator-determinism-philox-81n6xm
branch
from
July 5, 2026 18:09
c62a1d1 to
017745e
Compare
…ngrade - The reference implementation now supports the spec-defined scalar output (empty shape attribute) for deterministic generators; the empty-shape guard remains only for the legacy "unspecified" path, which cannot produce scalars. Evaluator test with the exact expected value added. - Corrected the nd_shape test docstring, which claimed the operator has no inputs — this PR itself added the optional offset input; the text is published verbatim in docs/Operators.md and docs/TestCoverage.md. - The 28->27 downgrade adapter now accepts a constant offset of 0 (Constant node or initializer, the documented pattern for storing the stream position in the model) and drops it together with the now-unused initializer, mirroring axis_input_to_attribute.h. Any other offset is still rejected. Regression tests for both cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp Signed-off-by: Timo Stripf <timo.stripf@emmtrix.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an optional
generatorattribute, a 64-bitseed_int64attribute, and an optionaloffsetinput toRandomUniform(new opset-28 schema), making the operator optionally deterministic — and therefore testable — while fully supporting streaming inference.RandomUniformwas previously the only generator op with no backend node tests at all, because its output could not be verified.Supersedes #2, which specified MT19937; this PR uses the counter-based Philox-4x32-10 instead (#2 stays open as backup).
generator="unspecified"(default value): the PRNG remains implementation-defined; fully backward compatible. This mode gives no determinism guarantee: results may differ across implementations and even across runs of the same implementation, even when a seed is specified. An implementation may produce reproducible results here, but is not required to. The legacy floatseedattribute applies only to this mode.generator="philox4x32_10": fully specified Philox-4x32 generator with 10 rounds (Salmon et al., "Parallel random numbers: as easy as 1, 2, 3", SC'11; standard constants M0=0xD2511F53, M1=0xCD9E8D57, W0=0x9E3779B9, W1=0xBB67AE85):seed_int64attribute (INT); its two's complement bits are the unsigned 64-bit Philox key —key0 = seed_int64 & 0xFFFFFFFF,key1 = (seed_int64 >> 32) & 0xFFFFFFFF. The legacy floatseedis not used in this mode and must not be set (both enforced by shape inference, as is rejection of unknown generator names). Rationale: ONNX attributes have exactly one fixed type andseedis float32, whose 24-bit significand cannot address a 64-bit key space; a dedicated INT attribute follows the establishedConstantvalue_int/value_floatpattern. (ONNX has no unsigned attribute type, so int64-bits-as-uint64 is the standard encoding, also used for theoffsetinput.)buses the 128-bit counter(lo32(b), hi32(b), lo32(offset), hi32(offset))and yields four 32-bit wordsw0..w3. For bfloat16/float16/float, elementiuses wordi mod 4of block⌊i/4⌋and formsr = ⌊w / 2^(32-p)⌋ / 2^pwithpsignificand bits (8/11/24) — exactly representable in the target type. For double, elementiuses words2*(i mod 2)and2*(i mod 2)+1of block⌊i/2⌋combined via the res53 scheme (r = (⌊a/2^5⌋·2^26 + ⌊b/2^6⌋) / 2^53).low + r * (high - low), withlow/highconverted todtypeand all arithmetic performed indtypeunder IEEE 754 round-to-nearest-even — bit-identical across conforming implementations. No double-precision arithmetic is needed unlessdtypeis double.seed_int64,offset, its positioni), so elements can be computed independently, in any order, or in parallel — stated normatively in the operator doc.Streaming: the
offsetinputA hidden "continue where the last run stopped" mode cannot exist in ONNX — a model run is a pure function of its inputs, and hidden state would be undefined under concurrent runs, session cloning, or replay. Instead, streaming is a wiring decision around one explicit input:
offset(int64 scalar, 0 if absent): keys Philox counter wordsc2/c3. Since the block index lives inc0/c1and the offset inc2/c3, streams of different offsets never overlap, regardless of the output size.Loop-carried value incremented in the graph. Each run remains individually deterministic and replayable, unlike hidden-state RNGs.The operator deliberately has no
next_offsetoutput:offset + 1is trivially computable (host counter,Addnode, loop-carried update), so such an output would carry no information — unlikepresent_state/KV-cache outputs, which cannot be derived without recomputation. It would also over-specify a chaining protocol where none is needed.The 28→27 downgrade adapter rejects nodes that use the
offsetinput or theseed_int64attribute (not expressible in older opsets).The attribute value set is deliberately extensible: the same pattern is intended to be rolled out to the other non-deterministic operators later (
RandomNormal,RandomUniformLike,RandomNormalLike,Bernoulli,Multinomial), and future opset versions can add further generator algorithms. The Philox implementation already lives in the shared_CommonRandombase of the reference runtime in preparation for that.Test cases and their intent
Every backend node test case documents its intent in a docstring that is published verbatim in
docs/TestCoverage.md; the reference-evaluator tests carry the same information as comments. Summary:test_randomuniform_philoxtest_randomuniform_philox_multi_blocktest_randomuniform_philox_nd_shapelow: row-major ordering must be rank-independent; sign handling inlow + r*(high-low).test_randomuniform_philox_offset.pbfile (offset=5), expected output from the stream disjoint to offset 0.test_randomuniform_philox_low_hightest_randomuniform_philox_doubletest_randomuniform_philox_float16/_bfloat16philox4x32 10known-answer vectors from the Random123 distribution (tests/kat_vectors), hardcoded inline.Multi-run
.pbdata: the backend test runner executes everytest_data_set_Ndirectory of a test case, so per-run input variation is expressible — the offset test uses exactly this mechanism (input_0.pbcarries the offset). Dynamic output shapes remain out of scope by construction:shapeis a required attribute; data-dependent shapes are the domain ofRandomUniformLike, which will gain the samegenerator/offsetmechanics in the planned follow-up.Changes
RandomUniform-28withgeneratorandseed_int64attributes and optionaloffsetinput (type constraintT2= int64); shape inference validates the generator value, requiresseed_int64for deterministic generators, and rejects the floatseedalongside them; old.cc: v22 schema preserved; operator_sets.h: registered under opset 28; new doc string with the complete, self-contained Philox-4x32-10 specification (round function, key schedule, counter mapping, offset/streaming semantics)CompatibleAdapter; 28→27 custom adapter that dropsgenerator="unspecified"and rejects deterministic generators, theseed_int64attribute, and theoffsetinput_Philox4x32class in_op_common_random.py(seed_int64 + offset keyed), verified against the known-answer vectors from the Random123 distribution and against the word stream of the canonical Random123 implementation; usesml_dtypes.finfoso non-native float types (bfloat16) resolve their precision correctlyRandomUniform(8 cases, see table above) with exact expected outputs generated by an independent inline Philox implementation in the test case, verified bit-exactly by the reference backend runnerseed_int64typing), shape inference (incl. error cases: missingseed_int64, floatseedwith philox, unknown generator), version converter 27↔28 (incl. offset andseed_int64rejection), reference evaluator (see table above)docs/Operators.md,docs/Changelog.md,docs/TestCoverage.md, and backend test dataMotivation and Context
The random-number operators are non-deterministic per spec: even with
seedset, results differ across implementations, so conformance tests cannot verify their output. As a consequence, the random operators are effectively invisible to conformance tracking such as the ONNX Backend Scoreboard: there are no node tests whose results a backend could be checked against. With an opt-in, fully specified generator (Philox-4x32-10 for now, extensible later), the output becomes reproducible and bit-exactly verifiable, so random operators can be covered by the standard backend test suite and their support becomes measurable on the scoreboard — while the default behavior, including its freedom to be non-deterministic, stays unchanged. Philox is counter-based, so the specification is order-independent and parallelizable — a natural fit for GPU and multi-threaded backends — and the explicitoffsetinput supports streaming inference with fresh, reproducible values per run.🤖 Generated with Claude Code
https://claude.ai/code/session_01PVuibCDPDmYP2zoMawf9sp