---
title: "Trace a trained C# model to a graph"
url: https://recursiveai.org/onemodel/cookbook/trace-trained-model-to-graph/
description: "Trace a trained C# neural network to a graph, keep the batch dimension symbolic, save ONNX and OneModel files, and compare eager and session results."
version: "1.3.0"
---

# Trace a trained C# model to a graph

**OneModel** is a free, open-source neural-network runtime for .NET from Recursive AI that runs ONNX models and trains models. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModel.Training) · [GitHub](https://github.com/recursive-ai-oss/onemodel-csharp) · [Docs](https://recursiveai.org/onemodel/docs/) · [Download example (.zip)](https://recursiveai.org/onemodel/cookbook/trace-trained-model-to-graph/trace-trained-model-to-graph.zip)

Tracing records a trained network’s operations as a static graph you can save and run without its training code. It is not request logging, an explanation of a classification decision or a training history.

```shell
dotnet add package RecursiveAI.OneModel.Training
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

The snippet needs `trained.checkpoint` from the example zip.

```csharp
using RecursiveAI.OneModel.Tensors;
using RecursiveAI.OneModel.Training;

// Input file is in the example zip.
using var model = new Linear(1, 1,
    new Generator(42));
Checkpoint.Load("trained.checkpoint", model);
using var example = Tensor.Zeros(
    DType.F32, 4, 1);
using var graph = model.Trace(
    example, "batch", null);
graph.Save("line.onemodel");
Console.WriteLine("Saved line.onemodel");
```

Prints:

```text
Saved line.onemodel
```

Unzip [the example](https://recursiveai.org/onemodel/cookbook/trace-trained-model-to-graph/trace-trained-model-to-graph.zip), then `dotnet run`.

`trained.checkpoint` is in the zip: a `Linear(1, 1)` layer trained on `(0, 1)`, `(1, 3)`, `(2, 5)`, `(3, 7)`. The full program below recreates it. Trace with batch size `4`; the named `batch` axis can change when you run the graph.

## The full program

The program trains the layer, writes the checkpoint, traces with an example batch of four, saves both formats, and runs a batch of three: `x = [4, 5, 6]`. It compares the reloaded graph with eager `Forward`.

```csharp
using RecursiveAI.OneModel.Graph;
using RecursiveAI.OneModel.Tensors;
using RecursiveAI.OneModel.Training;

using var model = new Linear(1, 1,
    new Generator(42));
using var trainX = Tensor.FromSpan<float>(
    [0, 1, 2, 3], 4, 1);
using var trainY = Tensor.FromSpan<float>(
    [1, 3, 5, 7], 4, 1);
using var optimizer = new Sgd(
    model.Parameters(), 0.05f);
for (int step = 0; step < 200; step++)
{
    using var scope = new ActivationScope();
    optimizer.ZeroGrad();
    var loss = Losses.Mse(
        model.Forward(trainX), trainY);
    loss.Backward();
    optimizer.Step();
}
Checkpoint.Save("trained.checkpoint", model);
using var example = Tensor.Zeros(DType.F32, 4, 1);
using var traced = model.Trace(
    example, "batch", null);
traced.Save("line.onemodel");
traced.SaveOnnx("line.onnx");
using var session = Session.Load("line.onemodel");
using var input = Tensor.FromSpan<float>(
    [4, 5, 6], 3, 1);
using var eager = model.Forward(input);
using var outputs = session.Run(
    new Dictionary<string, Tensor>
        { ["input"] = input });
float[] expected = eager.ToArray<float>();
float[] actual = outputs["output"].ToArray<float>();
Console.WriteLine("Eager: " + string.Join(", ",
    expected.Select(x => $"{x:F3}")));
Console.WriteLine("Graph: " + string.Join(", ",
    actual.Select(x => $"{x:F3}")));
float difference = expected.Zip(actual,
    (a, b) => Math.Abs(a - b)).Max();
Console.WriteLine(
    $"Largest difference: {difference:F6}");
Console.WriteLine(
    "Saved line.onemodel and line.onnx");
```

`dotnet run` prints:

```text
Eager: 9.002, 11.003, 13.003
Graph: 9.002, 11.003, 13.003
Largest difference: 0.000000
Saved line.onemodel and line.onnx
```

Reading traced tensor data, changing it in place or recording unsupported operations throws `TraceException` naming the operation.

## Variations

To export the trace as ONNX, replace the save line in the snippet at the top with:

```csharp
graph.SaveOnnx("line.onnx");
Console.WriteLine("Saved line.onnx");
```

```text
Saved line.onnx
```

## Related recipes

- [Train a neural network](https://recursiveai.org/onemodel/cookbook/train-a-model/)
- [Run an ONNX model](https://recursiveai.org/onemodel/cookbook/run-onnx-model/)
- [Convert to the OneModel format](https://recursiveai.org/onemodel/cookbook/convert-to-onemodel-format/)

## The short answer

Call `NnModule.Trace` with an example tensor and names for any axes that must remain symbolic. Save the resulting `TracedModel` with `Save` for the OneModel format or `SaveOnnx` for ONNX. The recorded graph runs through an ordinary inference `Session`; it no longer needs the training layer object to execute.


## What is model tracing?

Here, model tracing means recording the tensor operations performed by a C# model during one example forward pass. The [tracing guide](https://recursiveai.org/onemodel/docs/guide-tracing/) describes the result as an ordinary graph with the same inference optimizations and file formats as an imported model.

An `NnModule` supplies the parameters and buffers. Operations that depend on a traced input become graph nodes; constant values used by those operations become initializers containing copies of their values at trace time. Later training updates do not rewrite an already saved graph. Trace and save again after changing the trained weights if you want to deploy the updated model.

`NnModule.Trace` temporarily puts the layer in evaluation mode and restores its previous mode afterward. Its default graph names are `input` and `output`, which is why the full program’s dictionary uses `input` rather than a name chosen arbitrarily by the caller. For several inputs, outputs or custom names, use `Tracer` directly as described in the [tracing reference](https://recursiveai.org/onemodel/docs/tracing/).

The eager comparison checks the recorded model, not training quality. The example layer approximates `y = 2x + 1`, so its output at `x = 4` is close to nine. Tracing preserves the learned parameters; it does not replace them with the exact mathematical formula. The printed difference in this run is zero at six decimal places. The docs allow last-bit differences where eager and graph operators sum differently, so do not generalize that formatted result to every network.

## Can a traced static graph use dynamic shapes?

Yes, for axes you declare symbolic. The `"batch"` argument names the first axis; `null` leaves the second axis fixed. The example tensor therefore has shape `[4, 1]`, while the full program’s inference tensor has shape `[3, 1]`. The session makes a plan for the concrete input shape when you run it.

Choose symbolic example sizes carefully. Each must be at least two and different from the other symbolic example sizes. Avoid sizes equal to fixed dimensions in the model, because the tracer uses example extents to follow symbolic axes through reshape operations. A name alone does not make every dimension in the model dynamic; the reference explains how shape expressions propagate through supported operations.

Dynamic dimensions also do not turn C# control flow into graph control flow. Loops over layers and branches on sizes or flags run once during tracing and are unrolled into the recorded structure. Decisions based on traced tensor values are refused rather than silently frozen. Reading their data, changing them in place, `GradMode.NoGrad()` and operations without graph equivalents throw `TraceException`. Losses, training-mode dropout and `CtcLoss` cannot be traced.

Trace the inference forward path after training, rather than the optimizer or loss computation. Then run representative shapes through the saved session and compare them with the corresponding eager forward pass. The complete program demonstrates that sequence without assuming that the example batch size is the only batch you will serve.
