NumPy function#

This page answers common “how do I…” questions for exporting a Python function that processes numpy arrays into ONNX.


How to export a function processing numpy arrays#

Use yobx.sql.trace_numpy_to_onnx() with a representative sample input. Only the input dtype and shape are used during export.

<<<

import numpy as np
from yobx.helpers.onnx_helper import pretty_onnx
from yobx.sql import trace_numpy_to_onnx


def normalize_rows(X):
    norms = np.sqrt(np.sum(X ** np.float32(2), axis=1, keepdims=True))
    safe_norms = np.where(norms < np.float32(1e-8), np.float32(1), norms)
    return X / safe_norms


X_sample = np.zeros((3, 4), dtype=np.float32)
onx = trace_numpy_to_onnx(normalize_rows, X_sample)
print(pretty_onnx(onx))

>>>

    opset: domain='' version=21
    opset: domain='ai.onnx.ml' version=1
    input: name='X' type=dtype('float32') shape=['batch', 4]
    init: name='init1_s_' type=float32 shape=() -- array([2.], dtype=float32)-- Opset.make_node.1/Small
    init: name='init7_s1_1' type=int64 shape=(1,) -- array([1])           -- Opset.make_node.1/Shape
    init: name='init1_s_2' type=float32 shape=() -- array([1.e-08], dtype=float32)-- Opset.make_node.1/Small
    init: name='init1_s_3' type=float32 shape=() -- array([1.], dtype=float32)-- Opset.make_node.1/Small
    Pow(X, init1_s_) -> _onx_pow_X
      ReduceSum(_onx_pow_X, init7_s1_1, keepdims=1) -> _onx_reducesum_pow_X
        Sqrt(_onx_reducesum_pow_X) -> _onx_sqrt_reducesum_pow_X
          Less(_onx_sqrt_reducesum_pow_X, init1_s_2) -> _onx_less_sqrt_reducesum_pow_X
          Where(_onx_less_sqrt_reducesum_pow_X, init1_s_3, _onx_sqrt_reducesum_pow_X) -> _onx_where_less_sqrt_reducesum_pow_X
            Div(X, _onx_where_less_sqrt_reducesum_pow_X) -> _onx_div_X
    output: name='_onx_div_X' type='NOTENSOR' shape=None

How to export a function with multiple inputs and outputs#

Use input_names and output_names when your function takes more than one input array and returns multiple arrays.

<<<

import numpy as np
from yobx.helpers.onnx_helper import pretty_onnx
from yobx.sql import trace_numpy_to_onnx


def combine(A, B):
    return A + B, A - B


A = np.random.randn(2, 3).astype(np.float32)
B = np.random.randn(2, 3).astype(np.float32)
onx = trace_numpy_to_onnx(
    combine,
    A,
    B,
    input_names=["A", "B"],
    output_names=["sum", "diff"],
)
print(pretty_onnx(onx))

>>>

    opset: domain='' version=21
    opset: domain='ai.onnx.ml' version=1
    input: name='A' type=dtype('float32') shape=['batch', 3]
    input: name='B' type=dtype('float32') shape=['batch', 3]
    Add(A, B) -> sum
    Sub(A, B) -> diff
    output: name='sum' type='NOTENSOR' shape=None
    output: name='diff' type='NOTENSOR' shape=None

How to validate ONNX outputs against numpy#

Run the exported graph with onnxruntime and compare with the original numpy implementation.

<<<

import numpy as np
import onnxruntime
from yobx.sql import trace_numpy_to_onnx


def normalize_rows(X):
    norms = np.sqrt(np.sum(X ** np.float32(2), axis=1, keepdims=True))
    safe_norms = np.where(norms < np.float32(1e-8), np.float32(1), norms)
    return X / safe_norms


rng = np.random.default_rng(0)
X_sample = rng.standard_normal((8, 4)).astype(np.float32)
X_test = rng.standard_normal((5, 4)).astype(np.float32)

onx = trace_numpy_to_onnx(normalize_rows, X_sample)
sess = onnxruntime.InferenceSession(
    onx.SerializeToString(), providers=["CPUExecutionProvider"]
)

(got,) = sess.run(None, {"X": X_test})
expected = normalize_rows(X_test)
print(f"max|diff|={np.abs(got - expected).max():.2e}")
assert np.allclose(got, expected, atol=1e-5)
print("Outputs match ✓")

>>>

    max|diff|=0.00e+00
    Outputs match ✓

See also

Numpy-Tracing and FunctionTransformer — implementation details on numpy tracing and supported operators.