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:
- Export it to
.ptefor your target backend using ExecuTorch Export Recipes. - 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 point | Integration |
|---|---|
| A supported task and a model from the catalog fit your goal | Use a pre-exported model and task hook |
| Your own model matches a built-in task's schema and preprocessing contract | Supply a custom configuration to that task's hook or factory |
| Your model needs different inputs, outputs or orchestration | Build 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
.pteand 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,
validateSpecthrowsSCHEMA_MISMATCHwith 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
inspectModelon 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
validateSpecin 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
{
"$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
inspectModelon the exported.pteand check the shapes, data types, and any dynamic dimensions resolved from yourget_model_schema. - The backend is registered —
getRegisteredBackendslists the backends compiled into the app. If the backend your model was lowered to is missing, execution fails withEXECUTION_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
- Schema Validation — the contract a custom model must satisfy, in depth.
- Native Libraries — enabling the hardware backend your model needs.
- Models & Tensors — running a
.ptedirectly, without a pipeline.