August 14, 2026 · 22 min read

Export PyTorch to NIR

NeuroCUDA exports a converted PyTorch SNN to NIR with neurocuda.to_nir after convert and compile. Install via pip install neurocuda or pip install neurocuda[all]. Source lives at https://github.com/Krishnav1/neurocuda under MIT. The file is not ONNX. Residual graphs use Kahn topological sort. Round-trip ResNet-18 is bit-exact at 0.000000 max abs diff.

Checkpoint to NIR file to round-trip check. This HowTo is the command path. It is not a definition of NIR and it is not a tool comparison.

TL;DR

To export PyTorch to NIR: pip install neurocuda[all] → load checkpoint → snn, meta = neurocuda.convert(model, calib_loader)neurocuda.finetuneneurocuda.compileneurocuda.to_nir(snn, "model.nir") → reload and assert max abs diff. Published ResNet-18 residual round-trip: 0.000000. File is not ONNX. Definition: what is NIR. Comparison: vs NIRTorch.

This page vs nearby URLs: this URL is the HowTo from a PyTorch checkpoint to a .nir file. /blog/what-is-nir-neuromorphic defines the format. /blog/neurocuda-vs-nirtorch compares NeuroCUDA to NIRTorch. /blog/convert-pytorch-to-snn converts without dwelling on export. Google should index this URL for the export query, not as a duplicate definition.

Export PyTorch to NIR with NeuroCUDA to_nir and bit-exact ResNet round-trip

Teams search export pytorch to NIR when they already have a .pth file and need a portable spiking graph. They do not need another essay on what neuromorphic intermediate representation means. They need a sequence that ends in a file on disk and a numeric check that the file still computes the same tensor. This page is that sequence.

NIR is the vendor-neutral graph format for spiking networks. NeuroCUDA writes it with neurocuda.to_nir. The published residual-graph result is a full ResNet-18 round-trip with 0.000000 maximum absolute difference. That number is the quality bar for this HowTo. If your export cannot be reloaded and compared, you have a serialization demo, not a verified export.

Export PyTorch to NIR from a checkpoint

Six steps. Do them in order. Skipping convert and jumping to to_nir on a ReLU ANN is not this protocol.

Step 1: Install NeuroCUDA

Use a venv. Install PyTorch first so the CUDA or CPU wheel is the one you intend. Then install NeuroCUDA. NIR extras live behind the [all] extra in many environments, so prefer that when the job is export.

python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install torch torchvision
pip install neurocuda
# NIR + NeuroBench + CartPole extras
pip install neurocuda[all]
python -c "import neurocuda; print(neurocuda.__version__)"

Package: pypi.org/project/neurocuda. Source (MIT): github.com/Krishnav1/neurocuda. Wheel pairing details: pip install neurocuda guide. Colab: NeuroCUDA Google Colab.

Step 2: Load the PyTorch checkpoint

Load a trained torch.nn.Module with ReLU activations. Keep the ANN object. You will need it as a sanity baseline even though the NIR file describes the spiking graph, not the original ReLU graph.

import torch
import torchvision.models as models

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = models.resnet18(weights=None, num_classes=10)
state = torch.load("resnet18_cifar10.pth", map_location=device)
model.load_state_dict(state)
model.eval()
model.to(device)

ResNet-18 on CIFAR-10 is the architecture with a published NIR identity check. If you are exporting a 3-layer N-MNIST CNN instead, the commands are the same; the published 0.000000 figure is the ResNet residual case. CIFAR-10 conversion context: ResNet-18 SNN conversion tutorial.

Step 3: Convert with a calibration loader

NIR export is not a replacement for conversion. QCFS calibration still needs in-distribution batches. convert returns two values: the spiking network and metadata.

from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import neurocuda

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465),
                         (0.2470, 0.2435, 0.2616)),
])
train = datasets.CIFAR10(root="./data", train=True, download=True, transform=transform)
cal_size = int(0.1 * len(train))
_, cal_set = random_split(train, [len(train) - cal_size, cal_size])
calib_loader = DataLoader(cal_set, batch_size=64, shuffle=True)

snn, meta = neurocuda.convert(model, calib_loader)
print(type(snn), type(meta))

Calibration theory is on QCFS ANN to SNN. The convert HowTo without export emphasis is convert PyTorch to SNN. Stay here if the deliverable is a .nir file.

Step 4: Fine-tune the spiking network

Call neurocuda.finetune before you freeze the graph into NIR. Exporting a freshly converted network that has not seen BPTT is allowed as an experiment. It is not how the published ResNet-18 accuracy row was produced. Fine-tune, then export the network you would actually deploy.

train_loader = DataLoader(train, batch_size=64, shuffle=True)
neurocuda.finetune(snn, train_loader)

Published ResNet-18/CIFAR-10 after this pipeline: SNN 94.61% ± 0.14% versus ANN 95.56% at T=32. N-MNIST (different checkpoint, different data): SNN 99.88% ± 0.02% versus ANN 99.70%. Those accuracy rows live in paper.pdf. The NIR row is separate: identity of the residual graph, not a second accuracy number.

Step 5: Compile a backend

Compile selects an execution backend for the live object. GPU and CPU are the software backends with published spike parity (0 deviations / 256000 spikes). Loihi 2 is an IF-neuron simulator versus published equations, not Loihi silicon. SpiNNaker-1 physical silicon is a different evidence class (EBRAINS jobs #420148 and #420186, 2-neuron smoke test, not ResNet-on-chip). Exporting NIR does not move those labels. A .nir file is portable syntax. It is not a silicon run.

neurocuda.compile(snn, target="gpu")
# neurocuda.compile(snn, target="cpu")
# neurocuda.compile(snn, target="loihi2_sim")  # simulator, not Loihi silicon

If you only needed a file for an offline tool, you might be tempted to skip compile. Keep it in the HowTo anyway: the round-trip check in step 6 needs a runnable object on at least one backend so you can compare tensors, not just file sizes.

Step 6: Write NIR and round-trip check

This is the step people meant when they typed export pytorch to NIR. Write the file. Load it back. Compare.

neurocuda.to_nir(snn, "resnet18_snn.nir")

# Prefer the repo helper for the published identity check:
#   python verify_nir_trained.py
#   bash benchmarks/reproduce.sh   # includes that helper after gate3

# Conceptual check the helper performs:
#   reload the .nir graph with NeuroCUDA's residual executor
#   run the same minibatch
#   print max abs diff to six decimals
# Published ResNet-18 residual round-trip: 0.000000

Use the repository script rather than a one-off loader. Residual graphs need the Kahn executor with multi-input sums. A naive node walk will not reproduce the 0.000000 row even if to_nir wrote a valid file.

python verify_nir_trained.py
# also invoked from:
bash benchmarks/reproduce.sh

The published ResNet-18 figure is bit-exact 0.000000 max abs diff. Print six decimal places. Rounding to "about zero" hides a skip-connection bug. A max abs diff of 1e-3 is not the published row even if classification accuracy still looks fine.

Write the NIR file, reload it, and demand 0.000000 on ResNet-18 residual graphs. File size is not a round-trip check. Accuracy alone is not a round-trip check.

The file is not ONNX

ONNX serializes conventional deep-learning graphs for runtimes that expect dense GEMMs and standard activations. NIR serializes spiking graphs: neurons, delays, and event-style nodes the neuromorphic stack understands. neurocuda.to_nir writes NIR. It does not write ONNX.

Practical consequences:

If a partner asks for ONNX, that is a different artifact from a different toolchain. If they ask for a portable spiking graph, this HowTo is the one. Format background (not commands) lives on what is NIR neuromorphic.

Residual graphs and Kahn topological sort

Feed-forward chains are easy to serialize: each node has one incoming tensor. ResNet is not a chain. A skip connection means a merge node has two (or more) incoming edges. The executor must wait until both branches are ready, then add them. If it assumes one input per node, the skip is dropped, duplicated, or applied in the wrong order. Classification can still look "almost right" while the hidden tensor is wrong. That is why this HowTo uses a max abs diff check instead of top-1 accuracy as the export gate.

NeuroCUDA's NIR executor walks the graph with Kahn topological sort: repeatedly execute nodes whose remaining inbound count is zero, then decrement successors. At merge nodes it performs explicit multi-input summation. That is the mechanism behind the published 0.000000 ResNet-18 round-trip. It is not a claim that every NIR tool in the ecosystem does the same. It is the claim you are checking when you export PyTorch to NIR with this compiler and then reload the file.

# conceptual order at a residual merge
#   conv_path  ----\
#                   + --> merge_sum --> next_block
#   skip_path  ----/
#
# Kahn: do not run merge_sum until both conv_path and skip_path completed.

If you want the comparison against NIRTorch's torch.fx tracing approach, that is a different URL: NeuroCUDA vs NIRTorch. This page does not rank that tool. It tells you how to produce and check a NeuroCUDA NIR file.

Helper scripts in the repo that already know this graph:

python examples/resnet_pipeline.py
python examples/convert_resnet.py
python verify_nir_trained.py

Use those when you are matching the published residual identity. Use a tiny MLP only to debug install problems. A linear stack will not exercise Kahn merge behavior, so a green MLP export does not prove the ResNet row.

What a successful export looks like

CheckPassNot a pass
File writtenmodel.nir exists and is non-emptyexception, zero-byte file
Reloadloader returns a graph without errorparser crash
ResNet-18 residual round-trip0.000000 max abs diffany non-zero max abs diff
Optional accuracySNN still near 94.61% at T=32using accuracy instead of identity
CPU vs GPU live SNN0 / 256000 spike deviationstreating wall-clock as identity
Siliconout of scope for this filecalling .nir a Loihi or SpiNNaker run

Accuracy after export should still match the live SNN if the round-trip is bit-exact. If identity holds and accuracy drops, you evaluated a different loader or a different T. If accuracy holds and identity fails, you are looking at a merge bug that classification rounding hid. Trust identity for this HowTo.

Honest limits of the NIR file

A verified NeuroCUDA NIR export does not mean:

Hub for backend labels: /neurocuda. CUDA field context: /neuromorphic-cuda. Reproduction of published accuracy (not this export HowTo): reproduce NeuroCUDA results.

Minimal script you can save

This is the whole HowTo in one file. Swap the checkpoint path and the DataLoader for N-MNIST if that is your model. For N-MNIST prep see examples/prep_nmnist.py and N-MNIST SNN conversion.

"""checkpoint -> NIR file -> round-trip check"""
import torch
import torchvision.models as models
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import neurocuda

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = models.resnet18(weights=None, num_classes=10)
model.load_state_dict(torch.load("resnet18_cifar10.pth", map_location=device))
model.eval().to(device)

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465),
                         (0.2470, 0.2435, 0.2616)),
])
train = datasets.CIFAR10("./data", train=True, download=True, transform=transform)
n_cal = int(0.1 * len(train))
_, cal = random_split(train, [len(train) - n_cal, n_cal])
calib_loader = DataLoader(cal, batch_size=64, shuffle=True)
train_loader = DataLoader(train, batch_size=64, shuffle=True)

snn, meta = neurocuda.convert(model, calib_loader)
neurocuda.finetune(snn, train_loader)
neurocuda.compile(snn, target="gpu" if device.type == "cuda" else "cpu")
neurocuda.to_nir(snn, "resnet18_snn.nir")
print("wrote resnet18_snn.nir", "meta", meta)

Then run python verify_nir_trained.py or your reload snippet and print max abs diff to six decimals. Log commit SHA and neurocuda.__version__ next to that number. An export without versions is not reviewable.

Debugging a bad export

  1. ImportError on to_nir. Install pip install neurocuda[all].
  2. Empty or tiny file. You exported before convert finished, or wrote to a path that another process truncated.
  3. Non-zero max abs diff on a linear MLP. Check dtype and device of the reload batch first. Then check whether you compared logits at different timesteps.
  4. Non-zero max abs diff on ResNet-18. You are in residual territory. Confirm you used NeuroCUDA's executor, not a single-input walk. Confirm T and the same minibatch.
  5. Accuracy crash, identity not yet measured. Fix conversion first with convert PyTorch to SNN. Exporting a dead network preserves death.
  6. Partner opened the file in an ONNX runtime. Remind them the file is not ONNX. Point at what is NIR.

Framework comparisons that are not this HowTo: NeuroCUDA vs SpikingJelly. SpikingJelly is a training stack. It does not replace to_nir in this sequence.

Where this sits in the NeuroCUDA API

The four calls you will see in every serious script:

snn, meta = neurocuda.convert(model, calib_loader)
neurocuda.finetune(snn, train_loader)
neurocuda.compile(snn, target="gpu")
neurocuda.to_nir(snn, "model.nir")

Convert and finetune create the spiking object. Compile chooses a live backend. to_nir freezes a portable graph. Mixing those jobs is how people publish a GPU accuracy number as if it were a NIR identity number, or a NIR file as if it were SpiNNaker silicon. Keep the artifacts named: .pth is the ANN, live snn is the compiled SNN, .nir is the graph file, EBRAINS job IDs are silicon smoke tests.

To match paper accuracy after you have a file, leave this page and follow reproduce NeuroCUDA results with python reproduce.py --quick (N-MNIST, about 4 minutes) and bash benchmarks/reproduce.sh for gate3 QCFS 3 seeds plus verify_nir_trained.py plus gate5_neurobench.py.

Primary sources

  1. NeuroCUDA GitHub (MIT), github.com/Krishnav1/neurocuda
  2. PyPI, pypi.org/project/neurocuda
  3. Technical report, quantaracore.in/neurocuda/paper.pdf
  4. Product hub, quantaracore.in/neurocuda
  5. NIR definition article, /blog/what-is-nir-neuromorphic

Frequently asked questions

How do I export PyTorch to NIR with NeuroCUDA?

Install with pip install neurocuda or pip install neurocuda[all], load a checkpoint, run snn, meta = neurocuda.convert(model, calib_loader), then neurocuda.finetune, neurocuda.compile, and neurocuda.to_nir(snn, 'model.nir'). Verify by reloading the file and checking max abs diff.

Is a NIR file the same as ONNX?

No. NIR is the Neuromorphic Intermediate Representation for spiking graphs. ONNX is a conventional deep-learning graph format. neurocuda.to_nir writes NIR, not ONNX. Do not rename .nir to .onnx and expect an ONNX runtime to execute spikes.

How do I check a NIR round-trip?

Export with neurocuda.to_nir, load the graph back, run the same minibatch, and compute max absolute difference against the pre-export SNN (or the residual executor path). Published ResNet-18 round-trip is bit-exact: 0.000000 max abs diff. The repo helper is verify_nir_trained.py.

Does NeuroCUDA handle ResNet skip connections in NIR?

Yes. Residual graphs need multi-input summation at merge nodes. NeuroCUDA's NIR executor uses Kahn topological sort plus explicit multi-input sums. That is the path verified bit-exact on full ResNet-18.

What is Kahn topological sort doing in this export?

Kahn's algorithm walks the NIR graph so a node runs only after all incoming edges are ready. Skip connections create extra incoming edges. A naive single-input executor drops or mis-orders those edges. Kahn sort plus explicit sums is how residual NIR stays bit-exact.

How is this different from the What is NIR page?

/blog/what-is-nir-neuromorphic defines the format. This page is the HowTo from a PyTorch checkpoint to a .nir file and a round-trip check. Use the definition page for ecosystem context; use this URL for commands.

How is this different from NeuroCUDA vs NIRTorch?

/blog/neurocuda-vs-nirtorch compares two tools. This page does not rank NIRTorch. It shows neurocuda.to_nir, compile, and the 0.000000 ResNet-18 check. If you need a comparison table, use the vs page.

Do I need pip install neurocuda[all] to export NIR?

Use pip install neurocuda[all] when NIR extras are optional in your environment. pip install neurocuda covers convert, finetune, and compile. If to_nir import-fails, install the [all] extra. Source: https://github.com/Krishnav1/neurocuda under MIT.

What max abs diff is published for ResNet-18 NIR?

0.000000 maximum absolute difference on the ResNet-18 residual-graph round-trip. That is bit-exact identity for that check, not a claim that every downstream chip runtime is bit-exact.

Can I export PyTorch to NIR before convert()?

No for this HowTo. neurocuda.to_nir expects the converted spiking network returned by convert (and typically finetune plus compile). Exporting a raw ReLU ANN as if it were an SNN graph is outside this protocol.

Start now: pip install neurocuda[all] · GitHub · Product hub · PDF report