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.
dotnet add package RecursiveAI.OneModel.TrainingRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
The snippet needs trained.checkpoint from the example zip.
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:
Saved line.onemodelUnzip the example, 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.
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:
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:
graph.SaveOnnx("line.onnx"); Console.WriteLine("Saved line.onnx");
Saved line.onnxRelated recipes
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 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.
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.