ONNX Types¶
Opaque Type¶
An Opaque type (TypeProto.Opaque) enables the definition of user-defined
types, beyond the built-in kinds (tensors, sequences, maps, optionals, and
sparse tensors) that ONNX defines directly in its proto schema. It is
identified by a (domain, name) pair, analogous to how a custom op is
identified by a (domain, op_type) pair: the meaning of an Opaque type is
defined by, and only needs to be understood by, the producer/consumer of
the custom-domain ops that use it. As with all ONNX types (including
Tensor), the ONNX spec does not define how a value of an Opaque type is
represented internally by a backend – that is entirely up to the
implementation. ONNX itself just treats an Opaque-typed value as an opaque
piece of data (identified solely by its domain and name) that gets
passed between nodes.
The name is required (an Opaque type must be named). The domain
follows the same convention used for operator domains: it is optional,
and an empty/unspecified domain is treated as equivalent to the
standard "ai.onnx" domain.
Use-cases¶
Opaque types let a custom domain introduce new kinds of values – along
with custom ops that produce/consume them – that are only meaningful to
the ops of that domain, without requiring any change to the ONNX spec
itself. This is useful, for example, to represent a stateful handle
(e.g., a file handle, a database connection, or a random-number
generator) that is created by one custom op and consumed by others. More
generally, the Opaque type also gives the ONNX standard itself a way to
introduce new built-in types in the future without needing to change the
TypeProto schema.
Example: a stateful random-number generator (RNG)¶
The example below illustrates using an Opaque type to represent a stateful
random-number generator (RNG). It uses two illustrative custom ops (in a
custom domain test.rng, not part of the ONNX spec):
CreateRNG(seed) -> rngcreates a new RNG (of Opaque typetest.rng.RNG) from an integer seed.RandomTensor(rng) -> Y, rng_outuses the given RNG to generate a tensorYof a requested shape (with values drawn, say, from a standard normal distribution), and also returns an updated RNGrng_out.
Note that since ONNX ops are (side-effect free) functions, RandomTensor
cannot simply mutate its input RNG in place to reflect the fact that
generating a random value conceptually advances the RNG’s internal state.
Instead, that state update is made explicit: the op returns a new/updated
RNG as an additional output, alongside the generated tensor. A caller that
wants to draw a sequence of random tensors would thread the RNG through a
sequence of calls to RandomTensor, using the rng_out from one call as
the rng input to the next.
This example is deliberately simple: it does not implement an actual RNG algorithm, nor does it pin down all the details (such as the precise semantics of the state update) that a real-world stateful-RNG design would need to address. Its purpose is just to illustrate how an Opaque type can be declared, produced, consumed, and type/shape-inferred.
An Opaque type can be written explicitly in ONNX’s text format (see
Syntax.md) using the syntax opaque(domain, name) (or
opaque(name) when no domain is needed, or plain opaque() when neither
is specified). A model using the CreateRNG and RandomTensor ops above,
expressed using ONNX’s text format, looks like this:
<
ir_version: 10,
opset_import: ["": 21, "test.rng": 1]
>
agraph (int64 seed) => (float[2,3] Y, opaque(test.rng, RNG) rng2)
{
rng = test.rng.CreateRNG (seed)
Y, rng2 = test.rng.RandomTensor <shape = [2, 3]> (rng)
}
Here, rng2 (the second graph output, produced by RandomTensor) is
explicitly declared with the Opaque type test.rng.RNG using the
opaque(test.rng, RNG) syntax. The intermediate value rng (produced by
CreateRNG) is left untyped in the source text above; running shape
inference on the parsed model determines (and fills in) its type, based on
the type/shape-inference function registered for the CreateRNG op
schema – intermediate and output values may always be left untyped in
this way and have their types filled in by shape inference. See
tests/python/opaque_type_test.py for a complete, runnable version of
this example (including the schema and type/shape-inference-function
definitions for CreateRNG and RandomTensor), which also checks that
the resulting model passes both onnx.checker.check_model and
onnx.shape_inference.infer_shapes.
Optional Type¶
An optional type represents a reference to either an element (could be Tensor, Sequence, Map, or Sparse Tensor) or a null value. The optional type appears in model inputs, outputs, as well as intermediate values.
Use-cases¶
Optional type enables users to represent more dynamic typing scenarios in ONNX. Similar to Optional[X] type hint in Python typing which is equivalent to Union[None, X], Optional types in ONNX may reference a single element, or null.
Examples in PyTorch¶
Optional type only appears in TorchScript graphs generated by jit script compiler. Scripting a model captures dynamic types where an optional value can be assigned either None or a value.
Example 1
class Model(torch.nn.Module): def forward(self, x, y:Optional[Tensor]=None): if y is not None: return x + y return xCorresponding TorchScript graph:
Graph( %self : __torch__.Model, %x.1 : Tensor, %y.1 : Tensor? ): %11 : int = prim::Constant[value=1]() %4 : None = prim::Constant() %5 : bool = aten::__isnot__(%y.1, %4) %6 : Tensor = prim::If(%5) block0(): %y.4 : Tensor = prim::unchecked_cast(%y.1) %12 : Tensor = aten::add(%x.1, %y.4, %11) -> (%12) block1(): -> (%x.1) return (%6)ONNX graph:
Graph( %x.1 : Float(2, 3), %y.1 : Float(2, 3) ): %2 : Bool(1) = onnx::OptionalHasElement(%y.1) %5 : Float(2, 3) = onnx::If(%2) block0(): %3 : Float(2, 3) = onnx::OptionalGetElement(%y.1) %4 : Float(2, 3) = onnx::Add(%x.1, %3) -> (%4) block1(): %x.2 : Float(2, 3) = onnx::Identity(%x.1) -> (%x.2) return (%5)Example 2
class Model(torch.nn.Module): def forward( self, src_tokens, return_all_hiddens=torch.tensor([False]), ): encoder_states: Optional[Tensor] = None if return_all_hiddens: encoder_states = src_tokens return src_tokens, encoder_statesCorresponding TorchScript graph:
Graph( %src_tokens.1 : Float(3, 2, 4,), %return_all_hiddens.1 : Bool(1) ): %3 : None = prim::Constant() %encoder_states : Tensor? = prim::If(%return_all_hiddens.1) block0(): -> (%src_tokens.1) block1(): -> (%3) return (%src_tokens.1, %encoder_states)ONNX graph:
Graph( %src_tokens.1 : Float(3, 2, 4), %return_all_hiddens.1 : Bool(1) ): %2 : Float(3, 2, 4) = onnx::Optional[type=tensor(float)]() %3 : Float(3, 2, 4) = onnx::If(%return_all_hiddens.1) block0(): -> (%src_tokens.1) block1(): -> (%2) return (%3)