Fluttorch
Ship a PyTorch model to a Flutter app, and fail the build when the export or the quantization moves the numbers.
fluttorch: ^1.0.0// generated from the manifest the exporter emitted, never hand-writtenfinal forecast = await SolarForecast.load(runtime, artifact: bytes);final out = await forecast.run(features: window);// and in the test suite, where the tolerance comes from the manifestawait expectParity( forecast.model, goldens: await DirectoryGoldenBundle.open('build/solar_forecast.fluttorch.json'),);A typed bridge between a PyTorch export and a Flutter app. The exporter writes the artifact, its manifest and its goldens in one command, and the Dart API is generated from the manifest. The goldens replay in your tests: the parity gate compares the outputs against the recorded tolerance and fails the build when they exceed it.
atol 0.1rtol 0.1cos ≥ 0.998The build stops here. The drift is 1.72 against a bound of 0.1, and the report names the tensor, the backend and the quantization it was measured with.
Putting a model on a phone means exporting it, and usually quantizing it. Both steps change the numbers, and nothing in the toolchain tells you when they change too much: the build succeeds, the app runs, the output looks plausible, and the model is quietly worse than the one you evaluated. Preprocessing goes wrong the other way round. Written once in Python for training and again in Dart for serving, the two copies drift apart at the first refactor. Fluttorch closes both gaps with one document. The exporter writes a manifest next to the artifact, the Dart API is generated from it, and the reference outputs captured during the export replay in the test suite. Shapes, dtypes, preprocessing, labels and the weight hash are emitted once by the exporter and read by three consumers: the code generator, the parity gate and the runtime. Nothing on the Dart side restates them, so nothing there can disagree with training. The gate is the reason this project exists. Typed bindings are a convenience; catching numerical drift is the part nothing else does. expectParity replays the goldens, measures the drift and fails the build with a report naming the tensor, the bound it broke and the backend that measured it. Where the export captured taps, the report also names the earliest layer whose numbers moved. The runtime refuses to load an artifact whose content hash is not the one its manifest recorded, which is what catches a re-export that updated only half of the pair. ExecuTorch is the first backend, not the architecture. A parity gate is worth the same on LiteRT and ONNX Runtime, and the runtime interface is the only seam a backend touches: above that line nothing knows which engine is executing the model. Capabilities are reported rather than assumed, because exposing intermediate activations or running deterministically is a property of the hardware and not of the build, and every report says which backend produced it. At 1.0.0 the API is frozen and the deprecation policy is written down: a deprecated API survives at least two minor releases, which is ExecuTorch's own policy rather than a number chosen here. The tolerances are measured against two models rather than started from, and the measurement said something you would not expect: the simpler model drifts up to eight times further, because its outputs are large and a relative error is measured against the output while the rounding happened on intermediates. What 1.0 does not promise is that every engine runs every model. ExecuTorch lowers VoltaCast and then fails to execute it, identically under its own Python runtime, and the suite records that as a failing expectation so the day upstream fixes it, the suite says so. The three native bindings stay off pub.dev, which cannot carry the native half.