Inline
Introduction
Spox implements the inline
function, which allows inlining existing ONNX models in your own Spox model. Any valid model can be used - it can be produced directly by your code, another converter library, or just prepared beforehand.
We’ll go over three main applications of inline
- composing existing models, embedding custom operators, and integration with existing converter libraries.
[1]:
import numpy as np
import onnx
import onnxruntime
from spox import argument, build, inline, Tensor, Var
import spox.opset.ai.onnx.v17 as op
def run(model: onnx.ModelProto, **kwargs) -> list[np.ndarray]:
return onnxruntime.InferenceSession(model.SerializeToString()).run(
None,
{k: np.array(v) for k, v in kwargs.items()}
)
Composition
First, prepare some example models to compose in Spox. We’ll build them into ONNX, moving them out of Spox entirely. The same principle can be applied to existing models for which you have access to the onnx.ModelProto
.
[2]:
x, y = argument(Tensor(float, (None,))), argument(Tensor(float, (None,)))
z = op.add(op.div(x, y), op.div(y, x)) # x/y + y/x
harmonic_model = build({'x': x, 'y': y}, {'z': z})
[3]:
v, t = argument(Tensor(float, (None,))), argument(Tensor(float, ()))
w = op.abs(op.sub(v, t)) # |v - t|
dist_model = build({'v': v, 't': t}, {'w': w})
To compose the models we’ll create a new set of arguments and pass them into the functions returned by inline(harmonic_model)
and inline(dist_model)
. Those can be either passed positionally or as keyword arguments, following the naming in the model input list. A dictionary is returned, with ordering as in the model output list and keys being the output names.
[4]:
a, b = argument(Tensor(float, ('N',))), argument(Tensor(float, ('N',)))
harmonic_res = inline(harmonic_model)(x=a, y=b) # Use kwargs & dict result
assert list(harmonic_res.keys()) == ['z']
c = harmonic_res['z']
[5]:
(d,) = inline(dist_model)(c, op.constant(value=np.array(2.0))).values()
In summary, using the models \(z = \frac{x}{y} + \frac{y}{x}\) and \(w = v - t\) we constructed \(d = \left| \frac{a}{b} + \frac{b}{a} - 2 \right|\).
[6]:
harmonic_dist2_model = build({'a': a, 'b': b}, {'d': d})
Custom operators
Not all operators may be pre-generated in Spox. A simple way to embed a custom operator in Spox is to use inline
with a hand-crafted call with the operator. A wrapper can also perform basic type checking and process some attributes. We’ll use the example of the non-standard Inverse
operator, available in onnxruntime
.
[7]:
def inverse(m: Var) -> Var:
# Asserts argument is a tensor and checks its dtype
dtype = m.unwrap_tensor().dtype
# Do a basic type check
if dtype not in (np.float32, np.float64):
raise TypeError("Input element type expected to be float32/float64.")
# We'll use this to construct type information for inlined model input/output
tensor_dtype = onnx.helper.np_dtype_to_tensor_dtype(dtype)
# Construct the inlined model
inverse_model = onnx.helper.make_model(
onnx.helper.make_graph(
[onnx.helper.make_node("Inverse", ["i"], ["o"], domain="com.microsoft")],
"inverse_graph",
[onnx.helper.make_tensor_value_info("i", tensor_dtype, (None, None))],
[onnx.helper.make_tensor_value_info("o", tensor_dtype, (None, None))]
),
opset_imports=[
onnx.helper.make_opsetid("", 16),
onnx.helper.make_opsetid("com.microsoft", 1)
]
)
# Inline the model into Spox - unpack the only output into m1
(m1,) = inline(inverse_model)(m).values()
return m1
In this case, Spox will use the type information in the constructed model to check that the passed argument is a matrix (tensor of rank 2). We additionally assert the dtype is float32
or float64
.
We can now use the wrapper in Spox.
[8]:
f = argument(Tensor(float, ('N', 'N')))
g = op.matmul(f, inverse(f))
eye_model = build({'f': f}, {'g': g})
The model performs the toy computation of \(F F^{-1}\), which obviously should equal \(\mathbf{I}\) (unless \(F\) isn’t invertible). We can check this is the case when eye_model
is run in ONNX Runtime:
[9]:
(result,) = run(eye_model, f=np.array([[3., 7.], [1., 3.]]))
assert np.isclose(result, np.eye(2)).all()
result
[9]:
array([[1.00000000e+00, 6.66133815e-16],
[0.00000000e+00, 1.00000000e+00]])
Converter libraries
Lastly, it is worth noting how Spox can use the product of other converter libraries. In practice this reduces to a simple routine: it is enough to make the existing library produce an onnx.ModelProto
which can be passed to inline
. The only requirement is knowledge of the signature (model inputs/outputs). Some metadata written to the model may be lost (like e.g. docstrings).
For example, we could use skl_model = skl2onnx.to_onnx(Pipeline([...]))
and then pass this to Spox via inline(skl_model)(...)
.
[ ]: