Verify an ONNX Model Without PyTorch
Export MobileNet to ONNX and verify deterministic CPU inference.
Introduction
30 Second Summary
A file that works on the computer where it was created can still fail when someone else tries to use it. Without a clear handoff test, a missing piece can stay hidden until the file moves.
In this project, you will export a pretrained MobileNet V3 Small model into one portable ONNX file. You will build a separate command-line harness that verifies the file with PyTorch reference data before running deterministic CPU inference through ONNX Runtime.
What You'll Build
Picture running a single command that checks your model before printing five labeled predictions for an image.
By the end of this project, you'll have:
- A portable model file at artifacts/mobilenet_v3_small.onnx that runs in a clean environment without PyTorch or TorchVision.
- Golden test vectors that expose numerical drift by comparing standalone output with the original model's output.
- A verification command that displays integrity, metadata, parity, and repeatability passes before showing five ImageNet labels.
- Secret Mission: Prove the SHA-256 integrity gate rejects a deliberately corrupted model before ONNX Runtime loads it.
Are there any prerequisites?
You need a Windows x64 computer with Visual Studio Code plus internet access for setup.
The setup verifies Python 3.13 before guiding you through installation if it is missing.
Before We Start
This step turns the project goal into a clear commitment before the hands-on work begins. You will export a pretrained MobileNet V3 Small model as a portable ONNX artifact for verification in a PyTorch-free edge runtime.
Create Isolated Python Environments
A shared Python environment can silently supply PyTorch when runtime code imports it by mistake. That would make a framework-coupled program look portable.
You will create two isolated virtual environments. The export environment builds the ONNX artifact. The runtime environment proves that the artifact can run without PyTorch.
In this step, get ready to:
- Prepare the project folder in Visual Studio Code with Python 3.13.17 available.
- Create separate export and runtime virtual environments.
- Prove package isolation through pinned dependency lists.
Verify Python and prepare the project folder
Every pinned package in this project supports Python 3.13. The Windows py launcher lets you check that exact interpreter before creating either environment.
Start with a standalone Windows PowerShell window. You will move into Visual Studio Code after the interpreter check passes.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search field.
- Press Enter to open Windows PowerShell.
- Check the installed Python runtimes by running these commands:
py list
py -V:3.13 --version
What do these checks prove?
- The py list command shows the Python runtimes registered with the Windows launcher.
- The py -V:3.13 --version command selects the Python 3.13 runtime directly.
- A successful version check proves that later commands can target the required interpreter.
✔️ I see Python 3.13.17
Your interpreter is ready. The launcher can now create both environments with Python 3.13.17.
ⓧ I see an older Python version
Your older interpreter can remain installed. Python supports multiple registered runtimes through the Windows launcher.
- Open the official Python Windows releases page in your browser.
- Select the release named Python 3.13.17.
- Select Download Windows installer (64-bit).
- Run the downloaded installer from your browser download list.
- Complete the installer using its default installation options.
- Open a fresh Windows PowerShell window through Windows search.
- Repeat the two version checks above.
ⓧ Python 3.13 is missing
The required interpreter needs to be installed before the environments can be created. The official Windows installer registers it with the Python launcher.
- Open the official Python Windows releases page in your browser.
- Select the release named Python 3.13.17.
- Select Download Windows installer (64-bit).
- Run the downloaded installer from your browser download list.
- Complete the installer using its default installation options.
- Open a fresh Windows PowerShell window through Windows search.
- Repeat the two version checks above.
The project folder gives every command one predictable starting location. Opening that folder as a Visual Studio Code workspace also points the integrated terminal at the correct location.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Select File from the menu bar.
- Select Open Folder....
- Select New Folder in the folder picker.
- Enter edge-inference-benchmarking as the folder name.
- Select Select Folder to open the new folder as a workspace.
Why does Workspace Trust appear?
Visual Studio Code may show a Workspace Trust dialog before it enables terminal access. You created this empty folder yourself, so you can trust its contents.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
- Select View from the menu bar.
- Select Terminal to open the integrated terminal.
- Confirm that the terminal shell label shows PowerShell.
You should now see edge-inference-benchmarking in the Explorer. The PowerShell terminal starts inside that folder.
Project folder or terminal missing?
- Use File followed by Open Folder... again if the Explorer does not show edge-inference-benchmarking.
- Leave Restricted Mode by trusting the empty folder if terminal access is blocked.
- Use Terminal followed by New Terminal if the terminal panel opens without a shell.
Help me fix my Visual Studio Code workspace or integrated PowerShell terminal.
Separate the exporter from the runtime
Each virtual environment has its own package directory. Default isolation keeps globally installed packages outside both environments.
The .venv-export environment receives the model framework. The .venv-runtime environment receives only deployment dependencies.
- Create both isolated environments with Python 3.13 by running this command in the integrated PowerShell terminal:
py -V:3.13 -m venv .venv-export .venv-runtime
What does this command do?
- The -V:3.13 selector binds both environments to Python 3.13.
- The venv module creates separate Python executables inside each environment.
- The command leaves --system-site-packages disabled. That default preserves the deployment boundary.
- Wait for the PowerShell prompt to return.
- Confirm that .venv-export appears in the Visual Studio Code Explorer.
- Confirm that .venv-runtime appears in the Visual Studio Code Explorer.
Both private Python environments now exist. Their package sets are still empty apart from the standard environment tools.
Environment folders missing?
- Confirm that the terminal path points to the edge-inference-benchmarking folder.
- Rerun the Python version check if the launcher cannot select Python 3.13.
- Refresh the Explorer if the command completes but the folders remain hidden.
Help me diagnose why my two Python virtual environment folders were not created.
Pin and install each environment
A requirements file records the exact package contract for one environment. Separate files prevent model-building libraries from leaking into the runtime.
- Select the file creation icon in the Explorer sidebar.
- Enter requirements-export.txt as the file name.
You should see requirements-export.txt listed directly inside edge-inference-benchmarking.
- Fill requirements-export.txt with the export dependencies by pasting this package list:
torch==2.14.1
torchvision==0.29.1
onnx==1.23.2
onnxscript==0.7.2
numpy==2.5.4
Pillow==13.0.0
What does the export environment contain?
- PyTorch 2.14.1 loads the pretrained model. TorchVision 0.29.1 supplies the model architecture plus its weights.
- ONNX 1.23.2 validates the exported graph. ONNX Script 0.7.2 supports the modern exporter.
- NumPy 2.5.4 stores verification fixtures. Pillow 13.0.0 creates the sample image.
- Save requirements-export.txt.
- Confirm that the Explorer no longer shows an unsaved-change marker beside requirements-export.txt.
Export requirements look different?
- Confirm that each package occupies its own line.
- Check every version against the reference block above.
- Remove any automatic .txt duplication from the file name.
Help me compare my export requirements file with the pinned package contract.
- Select the file creation icon in the Explorer sidebar.
- Enter requirements-runtime.txt as the file name.
You should now see both requirements files in the Explorer. The second file defines the smaller deployment package set.
- Fill requirements-runtime.txt with the runtime dependencies by pasting this package list:
onnxruntime==1.31.0
numpy==2.5.4
Pillow==13.0.0
What does the runtime environment contain?
- ONNX Runtime 1.31.0 executes the exported model.
- NumPy 2.5.4 handles tensor preparation plus numerical checks.
- Pillow 13.0.0 handles image decoding plus resizing.
- PyTorch and TorchVision stay outside this environment. Their absence makes framework independence testable.
- Save requirements-runtime.txt.
- Confirm that the Explorer no longer shows an unsaved-change marker beside requirements-runtime.txt.
Runtime requirements look different?
- Confirm that the file contains exactly three package lines.
- Remove torch if it appears in the runtime file.
- Remove torchvision if it appears in the runtime file.
Help me verify that my runtime requirements file excludes framework packages.
✔️ Awesome, I've got everything!
Both requirements files now match the pinned environment contracts. Make sure each file is saved before installing its packages.
ⓧ I'd like to double check the full code
torch==2.14.1
torchvision==0.29.1
onnx==1.23.2
onnxscript==0.7.2
numpy==2.5.4
Pillow==13.0.0
onnxruntime==1.31.0
numpy==2.5.4
Pillow==13.0.0
The export installation is the larger package download. It can sit quietly for several minutes while PowerShell retrieves the framework files.
- Install the pinned export dependencies into .venv-export by running this command:
.\.venv-export\Scripts\python.exe -m pip install -r requirements-export.txt
Why use the environment executable?
The command invokes the Python executable stored inside .venv-export. Its pip process can only install the package pins from requirements-export.txt into that environment.
- Wait for the PowerShell prompt to return.
- Confirm that the installation output reports a successful completion.
Export installation failed?
- Confirm that the terminal remains connected to the internet.
- Check that requirements-export.txt is saved directly inside edge-inference-benchmarking.
- Rerun the Python version check if the output mentions an incompatible interpreter.
Help me diagnose my pinned export dependency installation.
The runtime installation is deliberately smaller. Its package set creates the clean deployment side of the boundary.
- Install the pinned runtime dependencies into .venv-runtime by running this command:
.\.venv-runtime\Scripts\python.exe -m pip install -r requirements-runtime.txt
How does this preserve isolation?
This command invokes pip through the runtime environment's own Python executable. Only the three entries in requirements-runtime.txt enter that environment.
- Wait for the PowerShell prompt to return.
- Confirm that the installation output reports a successful completion.
Runtime installation failed?
- Confirm that .venv-runtime exists in the Explorer.
- Check that requirements-runtime.txt contains the three exact package pins.
- Confirm that the terminal remains connected to the internet.
Help me diagnose my standalone runtime dependency installation.
Before you run the final check, what differences do you expect between the export environment and the runtime environment?
- Verify the interpreter plus the runtime package boundary by running these commands:
.\.venv-export\Scripts\python.exe --version
.\.venv-runtime\Scripts\python.exe -m pip list
What does this final check prove?
- The first command asks the export environment's executable to report its interpreter version.
- The second command lists packages visible to the runtime environment only.
- Together they expose the deployment boundary before any model code exists.
The version check should show Python 3.13.17.
The runtime list should include onnxruntime at 1.31.0. It should show numpy at 2.5.4.
A Pillow entry should show version 13.0.0. You should not see torch or torchvision.
Unexpected packages in the runtime?
- Confirm that the package-list command uses the executable inside .venv-runtime.
- Check that the environment was created without --system-site-packages.
- Recheck requirements-runtime.txt if either framework package appears.
Help me find why torch or torchvision appears in my isolated runtime environment.
That boundary is now real: the exporter has its framework tools while the runtime stays PyTorch-free. Next, you will export MobileNet V3 Small into a validated ONNX artifact.
Export and Validate the ONNX Model
Your isolated export environment now holds PyTorch plus the packages needed to convert pretrained weights into a portable ONNX artifact.
A model file becomes trustworthy when its graph passes validation. Its hash plus its labels plus its preprocessing contract plus its reference vectors also need to travel with it.
This step creates that evidence alongside the export. The result is a single model file with a known contract.
In this step, get ready to:
- Build an exporter that writes MobileNet V3 Small to one ONNX file.
- Capture deterministic fixtures plus labels plus a verification manifest.
- Run the completed exporter twice to confirm the artifact contract.
Build the exporter foundation
The exporter needs stable paths for every generated artifact. It also needs one helper that calculates the model's SHA-256 digest.
- Select the Explorer icon in the left sidebar of Visual Studio Code.
- Click the New File button at the top of the Explorer sidebar.
- Name the file export_model.py.
- Add the imports plus artifact paths plus hash helper by pasting this code into export_model.py:
from hashlib import sha256
import json
from pathlib import Path
import torch
import torchvision
from torchvision.models import MobileNet_V3_Small_Weights, mobilenet_v3_small
import numpy as np
import onnx
from PIL import Image
ARTIFACT_DIR = Path("artifacts")
MODEL_PATH = ARTIFACT_DIR / "mobilenet_v3_small.onnx"
MANIFEST_PATH = ARTIFACT_DIR / "manifest.json"
LABELS_PATH = ARTIFACT_DIR / "imagenet_classes.json"
GOLDEN_INPUT_PATH = ARTIFACT_DIR / "golden_input.npy"
GOLDEN_OUTPUT_PATH = ARTIFACT_DIR / "golden_logits.npy"
SAMPLE_IMAGE_PATH = ARTIFACT_DIR / "sample_image.png"
def sha256_file(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
What does this code set up?
- The imports bring in PyTorch plus TorchVision plus NumPy plus ONNX plus Pillow.
- The path constants keep every generated file inside the artifacts folder.
- The sha256_file() helper converts the model bytes into a hexadecimal integrity digest.
- Save export_model.py.
- Confirm that export_model.py now appears beside both requirements files in the Explorer sidebar.
Cannot find the exporter file?
Check that export_model.py sits directly inside the edge-inference-benchmarking folder. Move it there if Visual Studio Code created it inside another folder.
Ask for help with the file location if it still does not appear: Help me find export_model.py in my Visual Studio Code project.
The first runnable slice loads the official pretrained weights. It freezes the model in evaluation mode before exporting a fixed zero-valued tensor.
- Append the model export plus ONNX validation plus script entry point to export_model.py by pasting this code below sha256_file():
def main() -> None:
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
weights = MobileNet_V3_Small_Weights.DEFAULT
model = mobilenet_v3_small(weights=weights).eval()
dummy_input = torch.zeros((1, 3, 224, 224), dtype=torch.float32)
onnx_program = torch.onnx.export(
model,
(dummy_input,),
input_names=["input"],
output_names=["logits"],
dynamo=True,
external_data=False,
)
onnx_program.save(str(MODEL_PATH))
onnx.checker.check_model(MODEL_PATH, full_check=True)
print("ONNX checker: PASS")
if __name__ == "__main__":
main()
How does the export work?
- The fixed dummy_input establishes one input named input with the model shape required by this project.
- The dynamo=True setting uses the recommended exporter path.
- The external_data=False setting keeps the weights inside one .onnx file.
- The checker performs full graph validation before the script reports a pass.
- Save export_model.py.
The initial run downloads the pretrained weights. Expect this run to take a few minutes while the download completes.
Before you run the exporter, do you expect the checker to approve the graph?
- Run the first export in the integrated PowerShell terminal with the export environment by running:
.\.venv-export\Scripts\python.exe export_model.py
What does this command prove?
The command runs export_model.py with the Python executable inside .venv-export. This guarantees that the exporter uses the pinned framework packages from the export environment.
You should see ONNX checker: PASS. You should also see mobilenet_v3_small.onnx inside the new artifacts folder.
Exporter stopped before the checker?
- Confirm that your internet connection is available for the initial pretrained-weight download.
- Confirm that the command starts with the Python executable inside .venv-export.
- Check that the import lines in export_model.py match the code block exactly.
Ask for help with the first export: Why did my MobileNet V3 Small ONNX export stop before graph validation?
Create the verification contract
A golden test vector stores a fixed input plus its trusted framework output. Later, the standalone runtime can process the same input and measure numerical parity.
- In export_model.py locate print("ONNX checker: PASS").
- Insert the fixture plus label plus sample-image code directly below that line:
expected_logits = model(dummy_input).numpy(force=True)
np.save(GOLDEN_INPUT_PATH, dummy_input.numpy(force=True), allow_pickle=False)
np.save(GOLDEN_OUTPUT_PATH, expected_logits, allow_pickle=False)
labels = weights.meta["categories"]
LABELS_PATH.write_text(json.dumps(labels, indent=2), encoding="utf-8")
Image.new("RGB", (320, 256), (64, 128, 192)).save(SAMPLE_IMAGE_PATH)
What does the fixture capture?
- The two .npy files preserve the zero-valued input plus the corresponding PyTorch logits without pickled data.
- The labels come from the metadata attached to the selected pretrained weights.
- The generated PNG gives the standalone program a predictable image to preprocess.
- Save export_model.py.
- Generate the fixtures with the updated exporter by running:
.\.venv-export\Scripts\python.exe export_model.py
What changes after this run?
The exporter now writes golden_input.npy plus golden_logits.npy plus imagenet_classes.json plus sample_image.png.
- Expand the artifacts folder in the Explorer sidebar.
- Confirm that the two fixtures plus the labels file plus the sample image appear beside the ONNX model.
Missing a fixture file?
Check that every new line stays inside main() with four spaces of indentation. Code placed below the script entry point does not run as part of the function.
Ask for help with the generated files: Why is export_model.py missing one of its golden fixtures or labels?
The manifest turns separate files into one machine-readable contract. It records the tensor interface plus preprocessing values plus comparison tolerances plus source package versions.
- In export_model.py locate the line that saves SAMPLE_IMAGE_PATH.
- Insert the manifest code directly below that line:
manifest = {
"model_file": MODEL_PATH.name,
"model_sha256": sha256_file(MODEL_PATH),
"input_name": "input",
"input_shape": [1, 3, 224, 224],
"output_name": "logits",
"output_shape": [1, len(labels)],
"golden_input_file": GOLDEN_INPUT_PATH.name,
"golden_output_file": GOLDEN_OUTPUT_PATH.name,
"labels_file": LABELS_PATH.name,
"sample_image_file": SAMPLE_IMAGE_PATH.name,
"preprocessing": {
"resize_shorter_side": 256,
"center_crop": 224,
"scale_divisor": 255.0,
"mean": [0.485, 0.456, 0.406],
"std": [0.229, 0.224, 0.225],
},
"parity_tolerances": {"rtol": 0.0001, "atol": 0.00001},
"source_versions": {
"torch": torch.__version__,
"torchvision": torchvision.__version__,
"onnx": onnx.__version__,
},
}
MANIFEST_PATH.write_text(
json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8"
)
Why keep a manifest?
- The model hash detects any future change to the ONNX file's bytes.
- The input plus output metadata defines the tensor contract expected by the runtime.
- The preprocessing section preserves the resize plus crop plus normalization values associated with the pretrained weights.
- The parity tolerances define this project's numerical regression gate.
- Save export_model.py.
- Write the manifest with the updated exporter by running:
.\.venv-export\Scripts\python.exe export_model.py
What does this run add?
The exporter calculates the current model digest after saving the ONNX file. It writes that digest plus the complete artifact contract to manifest.json.
- Select manifest.json in the Explorer sidebar.
- Confirm that it contains the model filename plus tensor names plus preprocessing settings plus parity tolerances plus source versions.
Manifest file missing or incomplete?
Make sure the entire manifest dictionary stays inside main(). Check that its closing brace lines up with the first character of manifest.
Ask for help with the contract: Why is my generated manifest.json missing fields?
Clear terminal output makes each generated artifact easy to locate. The final lines also expose the exact digest stored in the manifest.
- Insert the artifact summary directly below the MANIFEST_PATH.write_text() call:
print(f"Model: {MODEL_PATH}")
print(f"SHA-256: {manifest['model_sha256']}")
print(f"Manifest: {MANIFEST_PATH}")
print(f"Golden input: {GOLDEN_INPUT_PATH}")
print(f"Golden output: {GOLDEN_OUTPUT_PATH}")
print(f"Labels: {LABELS_PATH}")
print(f"Sample image: {SAMPLE_IMAGE_PATH}")
What will the summary show?
These lines print the model digest plus the path of every generated file. The visible summary becomes the final export checkpoint.
- Save export_model.py.
- Compare the completed file against the full reference below.
Missing a summary line?
Check that all seven print() calls remain inside main() above the script entry point.
Ask for help with the completed script: Help me compare my export_model.py with the full exporter.
✔️ Awesome, I've got everything!
Great. Double-check that export_model.py is saved before continuing.
ⓧ I'd like to double check the full code
from hashlib import sha256
import json
from pathlib import Path
import torch
import torchvision
from torchvision.models import MobileNet_V3_Small_Weights, mobilenet_v3_small
import numpy as np
import onnx
from PIL import Image
ARTIFACT_DIR = Path("artifacts")
MODEL_PATH = ARTIFACT_DIR / "mobilenet_v3_small.onnx"
MANIFEST_PATH = ARTIFACT_DIR / "manifest.json"
LABELS_PATH = ARTIFACT_DIR / "imagenet_classes.json"
GOLDEN_INPUT_PATH = ARTIFACT_DIR / "golden_input.npy"
GOLDEN_OUTPUT_PATH = ARTIFACT_DIR / "golden_logits.npy"
SAMPLE_IMAGE_PATH = ARTIFACT_DIR / "sample_image.png"
def sha256_file(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
def main() -> None:
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
weights = MobileNet_V3_Small_Weights.DEFAULT
model = mobilenet_v3_small(weights=weights).eval()
dummy_input = torch.zeros((1, 3, 224, 224), dtype=torch.float32)
onnx_program = torch.onnx.export(
model,
(dummy_input,),
input_names=["input"],
output_names=["logits"],
dynamo=True,
external_data=False,
)
onnx_program.save(str(MODEL_PATH))
onnx.checker.check_model(MODEL_PATH, full_check=True)
print("ONNX checker: PASS")
expected_logits = model(dummy_input).numpy(force=True)
np.save(GOLDEN_INPUT_PATH, dummy_input.numpy(force=True), allow_pickle=False)
np.save(GOLDEN_OUTPUT_PATH, expected_logits, allow_pickle=False)
labels = weights.meta["categories"]
LABELS_PATH.write_text(json.dumps(labels, indent=2), encoding="utf-8")
Image.new("RGB", (320, 256), (64, 128, 192)).save(SAMPLE_IMAGE_PATH)
manifest = {
"model_file": MODEL_PATH.name,
"model_sha256": sha256_file(MODEL_PATH),
"input_name": "input",
"input_shape": [1, 3, 224, 224],
"output_name": "logits",
"output_shape": [1, len(labels)],
"golden_input_file": GOLDEN_INPUT_PATH.name,
"golden_output_file": GOLDEN_OUTPUT_PATH.name,
"labels_file": LABELS_PATH.name,
"sample_image_file": SAMPLE_IMAGE_PATH.name,
"preprocessing": {
"resize_shorter_side": 256,
"center_crop": 224,
"scale_divisor": 255.0,
"mean": [0.485, 0.456, 0.406],
"std": [0.229, 0.224, 0.225],
},
"parity_tolerances": {"rtol": 0.0001, "atol": 0.00001},
"source_versions": {
"torch": torch.__version__,
"torchvision": torchvision.__version__,
"onnx": onnx.__version__,
},
}
MANIFEST_PATH.write_text(
json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8"
)
print(f"Model: {MODEL_PATH}")
print(f"SHA-256: {manifest['model_sha256']}")
print(f"Manifest: {MANIFEST_PATH}")
print(f"Golden input: {GOLDEN_INPUT_PATH}")
print(f"Golden output: {GOLDEN_OUTPUT_PATH}")
print(f"Labels: {LABELS_PATH}")
print(f"Sample image: {SAMPLE_IMAGE_PATH}")
if __name__ == "__main__":
main()
What should match?
This reference combines the exporter plus graph check plus fixture generation plus manifest creation plus output summary. Your saved file should match it exactly.
Run and inspect the completed export
The completed exporter now creates the model plus every file needed to verify it later. Running it again confirms that the pinned export environment can reproduce the same artifact contract.
Before you run the completed script, which success message should appear before the artifact paths?
- Run the completed exporter with the export environment by running:
.\.venv-export\Scripts\python.exe export_model.py
What proves the export worked?
- The ONNX checker: PASS line confirms full graph validation completed.
- The SHA-256: line exposes the digest stored in the manifest.
- The remaining lines identify the model plus manifest plus fixtures plus labels plus sample image.
You should see ONNX checker: PASS followed by a SHA-256 digest. You should then see the path of every generated artifact.
- Expand the artifacts folder in the Explorer sidebar.
- Confirm that it contains mobilenet_v3_small.onnx plus manifest.json plus imagenet_classes.json plus golden_input.npy plus golden_logits.npy plus sample_image.png.
Before the repeat run, do you expect the checker plus artifact summary to complete with the same contract?
- Repeat the completed export by running:
.\.venv-export\Scripts\python.exe export_model.py
What does the repeat run confirm?
The second completed run rebuilds the model plus its companion files under the pinned environment. The same validation message plus artifact paths confirm that the export contract remains intact.
You should again see ONNX checker: PASS plus the digest plus all generated paths. The model is now packaged with the evidence needed for isolated runtime verification.
Repeat export no longer passes?
- Confirm that export_model.py still matches the full reference.
- Confirm that the command uses the Python executable inside .venv-export.
- Check that no generated file is open in another program that prevents the exporter from replacing it.
Ask for help with the repeat run: Why did my completed ONNX export stop passing on a repeat run?
Strong work. Your portable model now has a validated graph plus an integrity digest plus a reproducible verification contract.
Next, you will test whether framework-coupled exporter code belongs inside the clean runtime boundary.
Prove the Runtime Boundary
Your exporter has already turned MobileNet V3 Small into a validated ONNX artifact. The next risk is an accidental dependency on the PyTorch stack that created it.
Reusing exporter code in deployment looks convenient. The isolated runtime gives you a direct way to test whether that shortcut truly works.
In this step, get ready to:
- Inspect the runtime package inventory.
- Confirm the exported model files remain available across the deployment boundary.
- Test whether exporter code runs with the runtime interpreter.
Inspect the clean runtime
A package inventory shows exactly what an isolated Python environment can import. This inventory proves whether the runtime contains only its deployment packages.
- Inspect the runtime environment by running this command in the integrated PowerShell terminal:
.\.venv-runtime\Scripts\python.exe -m pip list
What does this command prove?
- The environment-specific Python executable ensures that the package list comes from .venv-runtime.
- The -m pip list portion asks that interpreter to report its installed packages.
- The resulting inventory makes the deployment boundary visible before you run any model code.
You should see ONNX Runtime at 1.31.0, NumPy at 2.5.4, plus Pillow at 13.0.0. You should not see torch or torchvision.
Can't inspect the runtime?
- Check that the VS Code Explorer still shows .venv-runtime beside requirements-runtime.txt.
- Confirm that .venv-runtime\Scripts\python.exe exists inside the environment folder.
- Return the terminal to the edge-inference-benchmarking folder if the relative executable path cannot be found.
Ask for help diagnosing the environment path.
The package environment stays isolated from the exporter. The generated files form the handoff between those environments.
- Expand the artifacts folder in the VS Code Explorer sidebar.
You should see mobilenet_v3_small.onnx, manifest.json, imagenet_classes.json, golden_input.npy, golden_logits.npy, plus sample_image.png.
Why does the artifact folder matter?
Files can cross the deployment boundary without bringing exporter packages with them. The model and its verification contract remain available while PyTorch and TorchVision stay outside the runtime.
Trigger the framework-coupled failure
The clean inventory and artifact folder establish the conditions for a decisive test. Reusing the exporter now puts the deployment shortcut under pressure.
Before you run this, do you think the exporter will complete inside an environment that contains only runtime packages?
- Test the exporter with the runtime interpreter by running this command:
.\.venv-runtime\Scripts\python.exe export_model.py
This failure is expected
PowerShell reports an import failure at import torch. The script stops before model construction or export begins.
That result proves export_model.py cannot cross the deployment boundary with its torch, torchvision, MobileNet_V3_Small_Weights, plus mobilenet_v3_small dependencies.
That planned failure is a win. Your runtime has exposed the framework dependency while every validated file in artifacts remains untouched.
Did the exporter complete?
- Confirm that the command used .venv-runtime\Scripts\python.exe instead of the export environment executable.
- Inspect the package list from earlier for an unexpected torch or torchvision entry.
- Check that requirements-runtime.txt still contains only the three pinned runtime packages.
Ask for help finding why the clean runtime can import PyTorch.
Your deployment boundary is now proven. Next, you will consume the exported files through ONNX Runtime without relying on the framework-coupled exporter.
Run Verified Standalone Inference
Your clean runtime already rejected export_model.py because PyTorch is absent. That planned failure proved that framework-coupled export code cannot cross your deployment boundary.
The exported ONNX artifact can cross that boundary. You will build a deployment program that uses ONNX Runtime for CPU inference. Pillow handles image operations. NumPy handles tensor preparation. A SHA-256 integrity gate checks the model before runtime session creation.
In this step, get ready to:
- Implement framework-free image preprocessing for the exported model.
- Verify model integrity before creating a deterministic CPU session.
- Prove golden-vector parity before printing five labeled predictions.
Implement preprocessing hygiene
A standalone runtime still needs to reproduce the image contract used by the pretrained weights. The program reads those preprocessing values from manifest.json so the artifact remains the source of truth.
Pillow decodes each image before resizing it. NumPy converts the pixels into a float32 tensor with shape [1, 3, 224, 224].
- Select the project folder in the Visual Studio Code sidebar.
- Use the new-file control to create standalone_infer.py inside the project folder.
- Add the runtime imports and artifact paths by pasting this code into standalone_infer.py:
import argparse
from hashlib import sha256
import json
from pathlib import Path
import numpy as np
import onnxruntime as ort
from PIL import Image
ARTIFACT_DIR = Path("artifacts")
DEFAULT_MODEL_PATH = ARTIFACT_DIR / "mobilenet_v3_small.onnx"
DEFAULT_MANIFEST_PATH = ARTIFACT_DIR / "manifest.json"
DEFAULT_IMAGE_PATH = ARTIFACT_DIR / "sample_image.png"
def sha256_file(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
def load_json(path: Path):
return json.loads(path.read_text(encoding="utf-8"))
What does this foundation do?
- The imports cover command-line parsing and local file access.
- The artifact constants point to the default model and its verification files.
- The two helpers calculate file digests and load JSON contracts.
- Save standalone_infer.py.
- Check that the foundation loads by running this command in the integrated PowerShell terminal:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should you see?
PowerShell returns to its prompt without a traceback. Your clean runtime can import every dependency used by this foundation.
Seeing an import error?
Confirm that the terminal is still inside the project folder. Check that you used the Python executable from .venv-runtime.
Ask for help with the runtime foundation.
- Add the resize and center-crop helper below load_json() by pasting this code:
def resize_shorter_side_and_center_crop(
image: Image.Image, resize_size: int, crop_size: int
) -> Image.Image:
width, height = image.size
if width < height:
new_width = resize_size
new_height = int(height * resize_size / width)
else:
new_height = resize_size
new_width = int(width * resize_size / height)
resized = image.resize(
(new_width, new_height), resample=Image.Resampling.BILINEAR
)
left = (new_width - crop_size) // 2
upper = (new_height - crop_size) // 2
return resized.crop((left, upper, left + crop_size, upper + crop_size))
How does the crop stay consistent?
- The shorter image side becomes the manifest resize size.
- The longer side keeps the original aspect ratio.
- The final crop takes an equal amount from both sides around the image center.
- Save standalone_infer.py.
- Check that the image helper loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What does this check prove?
PowerShell returns without a traceback. The resize helper is valid Python and can resolve the Pillow image types.
Seeing an image helper error?
Check the indentation inside both branches. Confirm that Image.Resampling.BILINEAR matches the code block exactly.
Ask for help with the resize helper.
- Add the tensor preprocessing helper below resize_shorter_side_and_center_crop() by pasting this code:
def preprocess_image(image_path: Path, manifest: dict) -> np.ndarray:
config = manifest["preprocessing"]
with Image.open(image_path) as opened:
image = opened.convert("RGB")
image = resize_shorter_side_and_center_crop(
image,
config["resize_shorter_side"],
config["center_crop"],
)
pixels = np.asarray(image, dtype=np.float32) / np.float32(
config["scale_divisor"]
)
channels_first = np.transpose(pixels, (2, 0, 1))
mean = np.asarray(config["mean"], dtype=np.float32)[:, None, None]
std = np.asarray(config["std"], dtype=np.float32)[:, None, None]
normalized = (channels_first - mean) / std
return normalized[None, ...]
What does the preprocessing produce?
- Pillow converts the source image to RGB before resizing it.
- NumPy scales the pixel values with the divisor stored in the manifest.
- The transpose changes the layout from height-width-channels to channels-height-width.
- The final indexing operation adds the batch dimension required by the model.
- Save standalone_infer.py.
- Check that the complete preprocessing path loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should happen now?
PowerShell returns without a traceback again. Your framework-free preprocessing functions now load inside the isolated runtime.
Preprocessing function not loading?
Check that preprocess_image() sits below the resize helper. Compare every manifest key with the code block.
Ask for help with the preprocessing function.
Add deterministic runtime gates
A successful model load only proves that the file is readable. Your harness must first prove that the bytes match the manifest. It must also prove that the tensor contract and reference output still match.
The CPU session uses one intra-op thread and one inter-op thread. Sequential execution plus deterministic compute creates a repeatable baseline for this pinned Windows environment.
- Add the deterministic session factory below preprocess_image() by pasting this code:
def create_session(model_path: Path) -> ort.InferenceSession:
options = ort.SessionOptions()
options.intra_op_num_threads = 1
options.inter_op_num_threads = 1
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
options.use_deterministic_compute = True
return ort.InferenceSession(
str(model_path),
sess_options=options,
providers=["CPUExecutionProvider"],
)
Why configure the session this way?
- The thread settings reduce scheduling variation inside the CPU baseline.
- Sequential execution gives graph operations a fixed execution mode.
- The provider list isolates inference to CPUExecutionProvider.
- Save standalone_infer.py.
- Check that the session configuration loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What does this check confirm?
PowerShell returns without a traceback. ONNX Runtime accepts each session option used by the deterministic CPU factory.
Session options causing an error?
Confirm that ort is the alias used by the import. Check each property name against the code block.
Ask for help with the session configuration.
- Add the integrity gate below create_session() by pasting this code:
def verify_integrity(model_path: Path, manifest: dict) -> None:
expected_hash = manifest["model_sha256"]
actual_hash = sha256_file(model_path)
if actual_hash != expected_hash:
raise RuntimeError(
f"Model hash mismatch: expected {expected_hash}, received {actual_hash}"
)
print("Integrity: PASS")
Why does integrity run first?
The manifest records the digest of the validated model. Any byte change produces a different digest and stops the program before session creation.
- Save standalone_infer.py.
- Check that the integrity gate loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should you observe?
PowerShell returns without a traceback. The integrity function is ready to compare the model bytes with the manifest digest.
Integrity gate not loading?
Check that sha256_file() remains near the top of the file. Confirm that the manifest key uses the exact spelling shown.
Ask for help with the integrity gate.
- Add the model metadata gate below verify_integrity() by pasting this code:
def verify_metadata(session: ort.InferenceSession, manifest: dict) -> None:
inputs = session.get_inputs()
outputs = session.get_outputs()
if len(inputs) != 1 or len(outputs) != 1:
raise RuntimeError("Expected exactly one model input and one model output")
if inputs[0].name != manifest["input_name"]:
raise RuntimeError("Input name does not match the manifest")
if list(inputs[0].shape) != manifest["input_shape"]:
raise RuntimeError("Input shape does not match the manifest")
if outputs[0].name != manifest["output_name"]:
raise RuntimeError("Output name does not match the manifest")
if list(outputs[0].shape) != manifest["output_shape"]:
raise RuntimeError("Output shape does not match the manifest")
if session.get_providers() != ["CPUExecutionProvider"]:
raise RuntimeError("The session is not isolated to CPUExecutionProvider")
print("Metadata: PASS")
What contract does this enforce?
- The session must expose exactly one input and one output.
- Both tensor names must match the manifest.
- Both tensor shapes must match the manifest.
- The active provider must be the CPU provider.
- Save standalone_infer.py.
- Check that the metadata gate loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What does this load check prove?
PowerShell returns without a traceback. The metadata comparisons can now inspect an ONNX Runtime session against the saved contract.
Metadata function not loading?
Check the indentation of every condition. Confirm that each manifest key matches the code block exactly.
Ask for help with the metadata gate.
- Add the golden-vector verification function below verify_metadata() by pasting this first part:
def verify_golden_vector(
session: ort.InferenceSession, manifest: dict
) -> np.ndarray:
golden_input = np.load(
ARTIFACT_DIR / manifest["golden_input_file"], allow_pickle=False
)
expected_output = np.load(
ARTIFACT_DIR / manifest["golden_output_file"], allow_pickle=False
)
input_name = manifest["input_name"]
output_name = manifest["output_name"]
repeated_outputs = [
session.run([output_name], {input_name: golden_input})[0]
for _ in range(5)
]
for repeated in repeated_outputs[1:]:
if not np.array_equal(repeated_outputs[0], repeated):
raise AssertionError("Repeated ONNX Runtime outputs were not identical")
tolerances = manifest["parity_tolerances"]
np.testing.assert_allclose(
repeated_outputs[0],
expected_output,
rtol=tolerances["rtol"],
atol=tolerances["atol"],
strict=True,
)
print("Golden parity: PASS")
How does the golden gate work?
- The fixtures load with pickled object data disabled.
- Five runs use the same fixed input and output name.
- Exact array equality checks repeat-run determinism.
- The parity assertion compares the first ONNX output with the saved PyTorch logits.
- Save standalone_infer.py.
- Check that the first part of the golden gate loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should happen?
PowerShell returns without a traceback. The file now contains the repeatability and numerical parity assertions.
Golden gate not loading?
Check the indentation inside the list comprehension. Confirm that the tolerance keys remain inside the np.testing.assert_allclose() call.
Ask for help with the golden gate.
- Add the determinism success message and return value directly below the existing golden parity message:
print("Repeat determinism (5 runs): PASS")
return repeated_outputs[0]
Why return the first output?
The function has already proved that all five outputs are identical. Returning the first output gives later code a verified result if the harness needs it.
- Save standalone_infer.py.
- Check that the completed golden gate loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What does this check confirm?
PowerShell returns without a traceback. The completed function can now report both parity and five-run determinism.
Seeing an indentation error?
Keep both new lines inside verify_golden_vector(). Align them with the existing golden parity message.
Ask for help placing the final lines.
Print top-five predictions
The verification gates establish trust before image inference begins. The remaining functions turn logits into probabilities and map the five highest values to the saved ImageNet labels.
- Add the probability conversion helper below verify_golden_vector() by pasting this code:
def softmax(logits: np.ndarray) -> np.ndarray:
shifted = logits - np.max(logits, axis=1, keepdims=True)
exponentials = np.exp(shifted)
return exponentials / np.sum(exponentials, axis=1, keepdims=True)
What does softmax do?
The maximum logit is removed before exponentiation to improve numerical stability. The normalized values become probabilities that sum to one across the classes.
- Save standalone_infer.py.
- Check that the probability helper loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should you see?
PowerShell returns without a traceback. The file can now convert a logits array into class probabilities.
Softmax helper not loading?
Confirm that each reduction uses axis=1. Check that both reductions preserve their dimensions.
Ask for help with the probability calculation.
- Add the ranked output function below softmax() by pasting this code:
def print_top_five(logits: np.ndarray, labels: list[str]) -> None:
probabilities = softmax(logits)
top_indices = np.argsort(probabilities[0])[-5:][::-1]
print("Top-5 predictions:")
for rank, index in enumerate(top_indices, start=1):
print(f"{rank}. {labels[index]}: {probabilities[0, index]:.6f}")
How are the results ranked?
- The sort returns indices ordered by probability.
- The final five indices select the largest values.
- The reverse slice places the most likely class first.
- Each printed row combines its rank with the matching label and probability.
- Save standalone_infer.py.
- Check that the ranking function loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What does this check prove?
PowerShell returns without a traceback. The output formatter is ready to print five labels with six-decimal probabilities.
Ranking function not loading?
Check the two slices after np.argsort(). Confirm that the formatted output line stays inside the loop.
Ask for help with the ranking function.
- Add the command-line parser below print_top_five() by pasting this code:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run verified MobileNet V3 Small ONNX inference on CPU."
)
parser.add_argument(
"image",
nargs="?",
type=Path,
default=DEFAULT_IMAGE_PATH,
help="Image path. Defaults to artifacts/sample_image.png.",
)
parser.add_argument(
"--model",
type=Path,
default=DEFAULT_MODEL_PATH,
help="ONNX model path.",
)
return parser.parse_args()
What inputs does the program accept?
- The optional image argument defaults to the generated sample image.
- The model option defaults to the validated model in artifacts.
- Both paths become Path objects before the program uses them.
- Save standalone_infer.py.
- Check that the command-line parser loads by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should happen?
PowerShell returns without a traceback. The parser is ready to accept either the default sample or a supplied image path.
Argument parser not loading?
Check the closing parentheses for both arguments. Confirm that return parser.parse_args() remains inside the function.
Ask for help with the command-line parser.
The main function sets the order of trust. Integrity runs before create_session(). Metadata and golden-vector verification run before image preprocessing.
- Add the program entry point below parse_args() by pasting this code:
def main() -> None:
args = parse_args()
manifest = load_json(DEFAULT_MANIFEST_PATH)
labels = load_json(ARTIFACT_DIR / manifest["labels_file"])
verify_integrity(args.model, manifest)
session = create_session(args.model)
verify_metadata(session, manifest)
verify_golden_vector(session, manifest)
print(f"Provider: {session.get_providers()[0]}")
model_input = preprocess_image(args.image, manifest)
logits = session.run(
[manifest["output_name"]],
{manifest["input_name"]: model_input},
)[0]
print_top_five(logits, labels)
if __name__ == "__main__":
main()
How does the full run flow?
- The manifest and labels load before verification begins.
- The model digest passes before the session is created.
- Metadata and golden fixtures verify the runtime contract.
- Image inference begins after every verification gate passes.
- Save standalone_infer.py.
Before you run the finished program, which verification message do you expect to appear before the provider name?
- Run the default sample through the finished verification harness by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py
What should the finished run show?
You should see Integrity: PASS before session-dependent checks. The terminal then prints Metadata: PASS.
You should also see Golden parity: PASS and Repeat determinism (5 runs): PASS. The provider line shows CPUExecutionProvider before five labeled predictions.
A verification gate failed?
- Confirm that standalone_infer.py is saved inside the same project folder as artifacts.
- Confirm that the original model and manifest still come from the same export run.
- Compare the failed function with the complete file below.
Ask for help with the failed verification gate.
✔️ Awesome, I've got everything!
Great. Double-check that standalone_infer.py is saved before testing another image.
ⓧ I'd like to double check the full code
import argparse
from hashlib import sha256
import json
from pathlib import Path
import numpy as np
import onnxruntime as ort
from PIL import Image
ARTIFACT_DIR = Path("artifacts")
DEFAULT_MODEL_PATH = ARTIFACT_DIR / "mobilenet_v3_small.onnx"
DEFAULT_MANIFEST_PATH = ARTIFACT_DIR / "manifest.json"
DEFAULT_IMAGE_PATH = ARTIFACT_DIR / "sample_image.png"
def sha256_file(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
def load_json(path: Path):
return json.loads(path.read_text(encoding="utf-8"))
def resize_shorter_side_and_center_crop(
image: Image.Image, resize_size: int, crop_size: int
) -> Image.Image:
width, height = image.size
if width < height:
new_width = resize_size
new_height = int(height * resize_size / width)
else:
new_height = resize_size
new_width = int(width * resize_size / height)
resized = image.resize(
(new_width, new_height), resample=Image.Resampling.BILINEAR
)
left = (new_width - crop_size) // 2
upper = (new_height - crop_size) // 2
return resized.crop((left, upper, left + crop_size, upper + crop_size))
def preprocess_image(image_path: Path, manifest: dict) -> np.ndarray:
config = manifest["preprocessing"]
with Image.open(image_path) as opened:
image = opened.convert("RGB")
image = resize_shorter_side_and_center_crop(
image,
config["resize_shorter_side"],
config["center_crop"],
)
pixels = np.asarray(image, dtype=np.float32) / np.float32(
config["scale_divisor"]
)
channels_first = np.transpose(pixels, (2, 0, 1))
mean = np.asarray(config["mean"], dtype=np.float32)[:, None, None]
std = np.asarray(config["std"], dtype=np.float32)[:, None, None]
normalized = (channels_first - mean) / std
return normalized[None, ...]
def create_session(model_path: Path) -> ort.InferenceSession:
options = ort.SessionOptions()
options.intra_op_num_threads = 1
options.inter_op_num_threads = 1
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
options.use_deterministic_compute = True
return ort.InferenceSession(
str(model_path),
sess_options=options,
providers=["CPUExecutionProvider"],
)
def verify_integrity(model_path: Path, manifest: dict) -> None:
expected_hash = manifest["model_sha256"]
actual_hash = sha256_file(model_path)
if actual_hash != expected_hash:
raise RuntimeError(
f"Model hash mismatch: expected {expected_hash}, received {actual_hash}"
)
print("Integrity: PASS")
def verify_metadata(session: ort.InferenceSession, manifest: dict) -> None:
inputs = session.get_inputs()
outputs = session.get_outputs()
if len(inputs) != 1 or len(outputs) != 1:
raise RuntimeError("Expected exactly one model input and one model output")
if inputs[0].name != manifest["input_name"]:
raise RuntimeError("Input name does not match the manifest")
if list(inputs[0].shape) != manifest["input_shape"]:
raise RuntimeError("Input shape does not match the manifest")
if outputs[0].name != manifest["output_name"]:
raise RuntimeError("Output name does not match the manifest")
if list(outputs[0].shape) != manifest["output_shape"]:
raise RuntimeError("Output shape does not match the manifest")
if session.get_providers() != ["CPUExecutionProvider"]:
raise RuntimeError("The session is not isolated to CPUExecutionProvider")
print("Metadata: PASS")
def verify_golden_vector(
session: ort.InferenceSession, manifest: dict
) -> np.ndarray:
golden_input = np.load(
ARTIFACT_DIR / manifest["golden_input_file"], allow_pickle=False
)
expected_output = np.load(
ARTIFACT_DIR / manifest["golden_output_file"], allow_pickle=False
)
input_name = manifest["input_name"]
output_name = manifest["output_name"]
repeated_outputs = [
session.run([output_name], {input_name: golden_input})[0]
for _ in range(5)
]
for repeated in repeated_outputs[1:]:
if not np.array_equal(repeated_outputs[0], repeated):
raise AssertionError("Repeated ONNX Runtime outputs were not identical")
tolerances = manifest["parity_tolerances"]
np.testing.assert_allclose(
repeated_outputs[0],
expected_output,
rtol=tolerances["rtol"],
atol=tolerances["atol"],
strict=True,
)
print("Golden parity: PASS")
print("Repeat determinism (5 runs): PASS")
return repeated_outputs[0]
def softmax(logits: np.ndarray) -> np.ndarray:
shifted = logits - np.max(logits, axis=1, keepdims=True)
exponentials = np.exp(shifted)
return exponentials / np.sum(exponentials, axis=1, keepdims=True)
def print_top_five(logits: np.ndarray, labels: list[str]) -> None:
probabilities = softmax(logits)
top_indices = np.argsort(probabilities[0])[-5:][::-1]
print("Top-5 predictions:")
for rank, index in enumerate(top_indices, start=1):
print(f"{rank}. {labels[index]}: {probabilities[0, index]:.6f}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run verified MobileNet V3 Small ONNX inference on CPU."
)
parser.add_argument(
"image",
nargs="?",
type=Path,
default=DEFAULT_IMAGE_PATH,
help="Image path. Defaults to artifacts/sample_image.png.",
)
parser.add_argument(
"--model",
type=Path,
default=DEFAULT_MODEL_PATH,
help="ONNX model path.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
manifest = load_json(DEFAULT_MANIFEST_PATH)
labels = load_json(ARTIFACT_DIR / manifest["labels_file"])
verify_integrity(args.model, manifest)
session = create_session(args.model)
verify_metadata(session, manifest)
verify_golden_vector(session, manifest)
print(f"Provider: {session.get_providers()[0]}")
model_input = preprocess_image(args.image, manifest)
logits = session.run(
[manifest["output_name"]],
{manifest["input_name"]: model_input},
)[0]
print_top_five(logits, labels)
if __name__ == "__main__":
main()
The sample proves that the runtime path works. A local image proves that the same verified path accepts input outside the generated artifact set.
- Replace the example image path with the full path to a local image.
Before you run the local image, do you expect the verification gates to change when only the image path changes?
- Run your local image through the same verification harness by running:
.\.venv-runtime\Scripts\python.exe standalone_infer.py "C:\path\to\image.jpg"
What should the local image run show?
The same four verification gates should pass. The provider should remain CPUExecutionProvider.
You should see five ImageNet labels with probabilities for your chosen image. The labels can differ from the sample because image inference happens after the fixed artifact checks.
Local image not loading?
- Replace the example path with a file that exists on your computer.
- Keep quotation marks around any path containing spaces.
- Confirm that Pillow can open the selected image format.
Ask for help with the local image path.
You have closed the deployment boundary. The clean runtime now verifies the model bytes and tensor contract before producing deterministic CPU predictions without PyTorch or TorchVision.
Secret mission
Reject a Tampered Model
Create a one-byte-corrupted copy of your ONNX model while preserving the approved artifact. Then prove the SHA-256 integrity gate rejects the changed file before ONNX Runtime creates an inference session.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local project, pause your work, or delete every file. This project creates no cloud resources or ongoing costs.
Resources you used:
- The .venv-export environment containing the pinned export packages.
- The .venv-runtime environment containing the standalone runtime packages.
- The artifacts folder containing every generated ONNX model artifact.
- The project scripts export_model.py, standalone_infer.py, and tamper_model.py.
- The pinned dependency files requirements-export.txt and requirements-runtime.txt.
- The edge-inference-benchmarking folder containing every local project resource.
Keep everything running
No action is needed. Choose this option if you want to keep testing the verification harness.
- Leave the edge-inference-benchmarking folder in its current location.
- Keep .venv-export for future model exports.
- Keep .venv-runtime for future standalone inference checks.
The environments use local disk space only. They do not create ongoing charges.
Pause - I'll come back to this later
Your Python commands exit after printing their results. Pausing means closing your local tools while preserving the complete verification harness.
- Save any open project files in Visual Studio Code.
- Close the integrated PowerShell terminal.
- Close Visual Studio Code.
Both virtual environments remain available inside edge-inference-benchmarking. Your original model remains unchanged inside artifacts.
Delete - I don't want to use this again
Remove every local project resource to start fresh.
Permanent deletion is final. Every artifact can be recreated by repeating the project.
Deleting the edge-inference-benchmarking folder removes both virtual environments. It also removes every script and generated artifact.
- In the Visual Studio Code Explorer sidebar, right-click requirements-runtime.txt.
- Choose the option that shows the selected file in File Explorer.
- Move up one directory in File Explorer.
You should now see the edge-inference-benchmarking folder from outside the project. This view lets you remove the complete project in one action.
- Switch back to Visual Studio Code.
- Close Visual Studio Code.
- Return to File Explorer.
- Select the edge-inference-benchmarking folder.
- Press Shift+Delete to permanently delete the folder.
- Confirm the permanent deletion.
You should no longer see edge-inference-benchmarking in File Explorer. Its absence confirms that the environments and model artifacts have been removed.
Nice Work!
Nice Work!
Excellent work! Your standalone ONNX verification harness now validates a portable MobileNet V3 Small model without PyTorch.
You've learned how to:
- Export a pretrained model as one portable ONNX artifact with a manifest plus golden test vectors.
- Run deterministic CPU inference through ONNX Runtime in an isolated environment without TorchVision.
- Protect every prediction with a SHA-256 integrity gate. Verify metadata plus numerical parity. Prove exact repeatability across five runs.
- Secret Mission: Prove that the integrity gate rejects a one-byte model change before ONNX Runtime creates a session.
Ready to quiz yourself?