Skip to main content
Version: Next

Exporting Custom Models

React Native ExecuTorch runs compatible ExecuTorch .pte files, including models outside our HuggingFace collection. Getting your own model running takes two things:

  1. Export it to .pte for your target backend using ExecuTorch Export Recipes.
  2. Integrate it — either feed it into a built-in pipeline by matching that pipeline's contract, or build your own pipeline around it, where you define the contract. Both come down to the model's schema.

The exporter provides model recipes, pinned environments and agent skills for the PyTorch → ExecuTorch step. This guide connects that workflow to integrating the result in a built-in pipeline or one you build yourself.

Choose your integration level​

Starting pointIntegration
A supported task and a model from the catalog fit your goalUse a pre-exported model and task hook
Your own model matches a built-in task's schema and preprocessing contractSupply a custom configuration to that task's hook or factory
Your model needs different inputs, outputs or orchestrationBuild a custom TypeScript pipeline on the model, tensor and worklet primitives

You can mix these approaches in one application. A custom model does not have to be contributed to the library registry, and a custom pipeline generally does not require app-specific C++.

Keep export and mobile integration in agreement​

Carry these artifacts from PyTorch through export to the app:

  • The .pte and every required tokenizer, vocabulary, label map or other auxiliary asset.
  • Exported method names, ordered inputs/outputs, data types, layouts, shapes and dynamic bounds.
  • Preprocessing and output-decoding rules, such as normalization, image resize/padding or sample rate.
  • The export/runtime version, backend requirements, and representative inputs with reference outputs.

Use an ExecuTorch export version and operators compatible with the native runtime shipped by the installed React Native ExecuTorch package. Match its enabled backends and target hardware; a .pte extension or successful schema inspection alone does not establish execution compatibility. See Native Libraries for app backend selection.

Preserve that contract when exporting. Compare exported outputs against the PyTorch reference before moving to mobile, with tolerances appropriate to the selected precision/backend.

Exporting to .pte​

Use ExecuTorch Export Recipes, our export harness, model recipes and agent skills. Its README covers setup and exporting; its repository instructions and skills guide agents through adding recipes, choosing a backend and validating outputs. Follow those instructions for your model and target device, and check compatibility with the native runtime shipped by your installed React Native ExecuTorch package.

For the underlying PyTorch → ExecuTorch APIs, see the official ExecuTorch documentation. Once validated, bring the .pte and the assets listed above into your React Native app.

What matters here is the contract the resulting .pte exposes: the input and output shapes, data types, and any dynamic dimensions or dimension relationships. That contract is what a pipeline validates against.

Inspect what your model exports​

Whichever way you integrate the model, start by seeing what it actually exports. inspectModel loads a .pte (from a URL or local path) and returns its schema and backends without wiring up a whole pipeline:

import { inspectModel } from 'react-native-executorch';

const { schema, backends } = await inspectModel('https://.../my-model.pte');
console.log(schema); // per-method inputs/outputs: shapes, data types, constraints
console.log(backends); // e.g. { forward: ['XnnpackBackend'] }

Using a built-in pipeline​

Every built-in pipeline accepts any model whose schema satisfies its contract — that is what Schema Validation checks at load time. To fit one, learn its contract and reconcile it with your model's schema. Three practical ways to learn the contract:

  • Read the mismatch. Point the pipeline at your model; if it doesn't fit, validateSpec throws SCHEMA_MISMATCH with a per-variant explanation of exactly which shape, dtype, or dimension didn't line up. The error message is the spec.
  • Inspect a reference model. Run inspectModel on the pre-exported model that pipeline uses from our HuggingFace collection. Whatever shapes it exports are, by definition, shapes the pipeline accepts — match them.
  • Read the pipeline source. Each pipeline's allowed spec is a few lines of validateSpec in its task file, readable in TypeScript.

Then line the two up: the data type and rank must match, each dimension must fall in the allowed domain, and any runtime constraints must be declared. A fully static model just needs matching constants; a model with variable dimensions needs the companion schema below.

Building a custom pipeline​

If no built-in pipeline fits your model, build your own on the primitives, where you define the contract. Load the model, declare the schema you expect with validateSpec (this documents the contract and guards against loading the wrong or a changed .pte), read the bound dimensions back to size your tensors, and wire preprocessing, execution, and postprocessing yourself:

import { loadModel, wrapAsync, schema } from 'react-native-executorch';

const { validateSpec, method, f32 } = schema;

const model = await wrapAsync(loadModel)('/path/to/model.pte');

// Declare and assert the contract this pipeline needs, then size tensors from it
const { dims } = validateSpec(model.schema, {
default: method('forward', [f32(1, 3, 'H', 'W')], [f32(1, 'N')]),
});
const [N, H, W] = dims.constant('N', 'H', 'W');
// ...allocate tensors of these shapes, then build the classify/execute closure

Wrap that construction in a resource scope so a failure part-way through — a validateSpec mismatch, say — releases the model already loaded instead of leaking it.

From here the rest of the section covers each piece: Models & Tensors for execution and tensor lifetime, Operations & Utilities for the preprocessing and postprocessing ops, Schema Validation for the contract DSL, and Worklets & Threading for running it off the JS thread.

Declaring dynamic shapes with get_model_schema​

When a .pte carries only static metadata, every dimension is read as a fixed constant. That is enough for fixed-shape models, but a model with a variable dimension (a sequence length, a dynamic batch) or a relationship between dimensions needs to say so explicitly — otherwise validation sees only the static upper bounds and rejects the model.

The mechanism is a companion method named get_model_schema that returns the model's schema as a JSON string. The native loader calls it at load time and overlays the precise range, enum, and constraint information onto the base metadata. See Where the exported spec comes from for how it's consumed.

You embed it during export by passing it as a constant method:

to_edge_transform_and_lower(
exported_program,
# ...
constant_methods={"get_model_schema": schema_json},
)

schema_json is a JSON encoding of the model's schema — a map from method name to its inputs, outputs, and runtime constraints, with each dimension given a concrete domain. For a text model with a dynamic sequence length L from 1 to 512:

{
"forward": {
"inputs": [
{
"kind": "Tensor",
"dtype": "int64",
"shape": [
{ "kind": "constant", "value": 1 },
{ "kind": "range", "range": { "min": 1, "max": 512, "step": 1 } }
]
}
],
"outputs": [
{
"kind": "Tensor",
"dtype": "float32",
"shape": [
{ "kind": "constant", "value": 1 },
{ "kind": "constant", "value": 768 }
]
}
],
"runtimeConstraints": []
}
}

Validate against the schema​

The payload is a ModelSpec over ConcreteDim dimensions — the same structure the native loader parses. Its format is the JSON Schema below: validate your generated JSON against it in your export pipeline so a malformed contract is caught at export time instead of on device, and register it in your editor for autocompletion and inline validation while authoring the JSON.

View the JSON Schema

model.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ModelSpec<ConcreteDim>",
"description": "Schema definition for ExecuTorch ModelSpec<ConcreteDim> describing exported model methods, parameters, dimension domains, and runtime constraints.",
"type": "object",
"additionalProperties": { "$ref": "#/definitions/MethodSpec" },
"definitions": {
"MethodSpec": {
"type": "object",
"properties": {
"inputs": { "type": "array", "items": { "$ref": "#/definitions/ParamSpec" } },
"outputs": { "type": "array", "items": { "$ref": "#/definitions/ParamSpec" } },
"runtimeConstraints": {
"type": "array",
"items": { "$ref": "#/definitions/RuntimeConstraint" }
}
},
"required": ["inputs", "outputs", "runtimeConstraints"],
"additionalProperties": false
},
"ParamSpec": {
"oneOf": [
{ "$ref": "#/definitions/TensorParamSpec" },
{ "$ref": "#/definitions/PrimitiveParamSpec" }
]
},
"TensorParamSpec": {
"type": "object",
"properties": {
"kind": { "const": "Tensor" },
"dtype": { "$ref": "#/definitions/DType" },
"shape": { "type": "array", "items": { "$ref": "#/definitions/ConcreteDim" } }
},
"required": ["kind", "dtype", "shape"],
"additionalProperties": false
},
"PrimitiveParamSpec": {
"type": "object",
"properties": {
"kind": {
"enum": [
"None",
"Int",
"Double",
"Bool",
"String",
"ListBool",
"ListDouble",
"ListInt",
"ListTensor"
]
}
},
"required": ["kind"],
"additionalProperties": false
},
"DType": { "type": "string", "enum": ["float32", "uint8", "int32", "int64"] },
"ConcreteDim": {
"oneOf": [
{ "$ref": "#/definitions/ConstantDim" },
{ "$ref": "#/definitions/RangeDim" },
{ "$ref": "#/definitions/EnumDim" }
]
},
"ConstantDim": {
"type": "object",
"properties": {
"kind": { "const": "constant" },
"value": { "type": "integer", "minimum": 1 }
},
"required": ["kind", "value"],
"additionalProperties": false
},
"RangeDim": {
"type": "object",
"properties": {
"kind": { "const": "range" },
"range": {
"type": "object",
"properties": {
"min": { "type": "integer", "minimum": 1 },
"max": { "type": "integer", "minimum": 1 },
"step": { "type": "integer", "minimum": 1 }
},
"required": ["min", "max", "step"],
"additionalProperties": false
}
},
"required": ["kind", "range"],
"additionalProperties": false
},
"EnumDim": {
"type": "object",
"properties": {
"kind": { "const": "enum" },
"choices": { "type": "array", "items": { "type": "integer", "minimum": 1 }, "minItems": 1 }
},
"required": ["kind", "choices"],
"additionalProperties": false
},
"RuntimeConstraint": {
"oneOf": [
{ "$ref": "#/definitions/EqualityConstraint" },
{ "$ref": "#/definitions/LinearConstraint" }
]
},
"EqualityConstraint": {
"type": "object",
"properties": {
"kind": { "const": "equality" },
"dims": { "type": "array", "items": { "$ref": "#/definitions/DimRef" }, "minItems": 2 }
},
"required": ["kind", "dims"],
"additionalProperties": false
},
"LinearConstraint": {
"type": "object",
"properties": {
"kind": { "const": "linear" },
"dimLhs": { "$ref": "#/definitions/DimRef" },
"dimRhs": { "$ref": "#/definitions/DimRef" },
"coefficients": {
"type": "array",
"items": { "type": "integer" },
"minItems": 2,
"maxItems": 2
}
},
"required": ["kind", "dimLhs", "dimRhs", "coefficients"],
"additionalProperties": false
},
"DimRef": {
"type": "object",
"properties": {
"paramSide": { "type": "string", "enum": ["input", "output"] },
"tensorIdx": { "type": "integer", "minimum": 0 },
"dimIdx": { "type": "integer", "minimum": 0 }
},
"required": ["paramSide", "tensorIdx", "dimIdx"],
"additionalProperties": false
}
}
}

Verify on device​

Before wiring a custom model into a screen, confirm two things on the device:

  • The schema is what you expect — run inspectModel on the exported .pte and check the shapes, data types, and any dynamic dimensions resolved from your get_model_schema.
  • The backend is registered — getRegisteredBackends lists the backends compiled into the app. If the backend your model was lowered to is missing, execution fails with EXECUTION_FAILED; enable it via the native library configuration.

Once both check out, load the model into the target pipeline and run it.

Compare a known inference result with the PyTorch/exported reference on the actual target backend. Also verify preprocessing, repeated calls, cancellation where supported, and resource cleanup. Export, inspection, a TypeScript build and device inference establish different parts of the workflow.

Where to go next​

API reference​