TensorFlow
C++ Integration
Graph Export
Machine Learning
Model Deployment

Tensorflow Different ways to Export and Run graph in C

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

The practical answer depends on which TensorFlow generation you are dealing with. In current TensorFlow workflows, SavedModel is the standard export format for serving and native inference, while raw GraphDef and frozen graph files are mostly legacy TensorFlow 1 patterns that you still see when maintaining older systems.

Preferred export path: SavedModel

TensorFlow's official guidance centers on SavedModel because it packages the graph, variable values, and named signatures together. That makes it much safer than exporting only a protobuf graph and hoping the runtime knows how to reconstruct weights and input names.

A small TensorFlow Python export example looks like this:

python
1import tensorflow as tf
2
3class Doubler(tf.Module):
4    @tf.function(input_signature=[tf.TensorSpec(shape=[None], dtype=tf.float32)])
5    def serve(self, x):
6        return {"y": x * 2.0}
7
8model = Doubler()
9tf.saved_model.save(model, "./saved_model", signatures={"serving_default": model.serve})

That directory now contains saved_model.pb, variables, and signature metadata. This is the format you should prefer if you control both export and deployment.

Running a SavedModel from native code

If you are using TensorFlow's native runtime, the usual route is the C++ API. The important point is that you load the model by tag and then run tensors through the restored session.

cpp
1#include <iostream>
2#include <vector>
3#include "tensorflow/cc/saved_model/loader.h"
4#include "tensorflow/core/framework/tensor.h"
5
6int main() {
7    tensorflow::SavedModelBundleLite bundle;
8    tensorflow::SessionOptions session_options;
9    tensorflow::RunOptions run_options;
10
11    auto status = tensorflow::LoadSavedModel(
12        session_options,
13        run_options,
14        "./saved_model",
15        {"serve"},
16        &bundle
17    );
18
19    if (!status.ok()) {
20        std::cerr << status.ToString() << std::endl;
21        return 1;
22    }
23
24    tensorflow::Tensor input(tensorflow::DT_FLOAT, tensorflow::TensorShape({3}));
25    auto flat = input.flat<float>();
26    flat(0) = 1.0f;
27    flat(1) = 2.0f;
28    flat(2) = 3.0f;
29
30    std::vector<tensorflow::Tensor> outputs;
31    status = bundle.GetSession()->Run(
32        {{"serving_default_x:0", input}},
33        {"StatefulPartitionedCall:0"},
34        {},
35        &outputs
36    );
37
38    if (!status.ok()) {
39        std::cerr << status.ToString() << std::endl;
40        return 1;
41    }
42
43    std::cout << outputs[0].flat<float>() << std::endl;
44}

The exact feed and fetch names depend on the exported signature. In practice, inspect the model with saved_model_cli before wiring native inference code.

Legacy option: export a frozen graph or GraphDef

Older TensorFlow 1 deployments often exported a GraphDef protobuf and, in some cases, a frozen graph where variables were converted into constants. This is still relevant when you inherit an older codebase, but it is not the preferred format for new work.

The main problem with a raw graph export is that you must keep track of input and output node names, and you may need extra logic for variables, checkpoints, and asset files. That makes deployment more brittle than SavedModel.

A minimal legacy export might look like this in TensorFlow 1 style code:

python
1import tensorflow as tf
2
3with tf.compat.v1.Session() as sess:
4    x = tf.compat.v1.placeholder(tf.float32, shape=[None], name="x")
5    y = tf.multiply(x, 2.0, name="y")
6    graph_def = sess.graph.as_graph_def()
7
8    with tf.io.gfile.GFile("graph.pb", "wb") as f:
9        f.write(graph_def.SerializeToString())

Use this only when you must interoperate with a legacy runtime that already expects it.

Low-level C API

If you truly need C rather than C++, TensorFlow also exposes a lower-level C API. The tradeoff is more boilerplate and less ergonomic model loading. It can import a graph definition and execute a session, but for modern deployments it is usually more work than using the C++ SavedModel loader or TensorFlow Lite.

c
1#include <tensorflow/c/c_api.h>
2
3int main(void) {
4    TF_Graph* graph = TF_NewGraph();
5    TF_Status* status = TF_NewStatus();
6
7    /* In real code, read graph.pb into a TF_Buffer before importing. */
8    TF_DeleteStatus(status);
9    TF_DeleteGraph(graph);
10    return 0;
11}

That API is useful when you have hard C integration constraints, but it is not the most convenient first choice.

TensorFlow Lite for smaller native deployments

If your actual requirement is native inference in a small binary, especially on mobile or edge devices, TensorFlow Lite is often a better fit than the full TensorFlow runtime. It has dedicated C and C++ APIs and is designed for inference rather than full graph execution.

So the real decision tree is:

  • full TensorFlow runtime and current export workflow: use SavedModel
  • legacy TensorFlow 1 maintenance: accept GraphDef or frozen graphs as needed
  • lightweight native inference: consider TensorFlow Lite instead

Common Pitfalls

A common mistake is exporting only a graph and then discovering at deployment time that the weights, signatures, or asset files were not preserved.

Another mistake is guessing feed and fetch tensor names instead of inspecting the exported signatures. Native inference code becomes fragile very quickly when names are hard-coded without verification.

A third mistake is choosing the full TensorFlow C API when the real need is lightweight inference. In many cases TensorFlow Lite is simpler operationally.

Summary

  • For modern TensorFlow deployments, SavedModel is the preferred export format.
  • Load and run SavedModel from native code through the TensorFlow C++ loader when possible.
  • Raw GraphDef and frozen graphs are mostly legacy TensorFlow 1 patterns.
  • The C API exists, but it is lower level and more cumbersome than the usual C++ path.
  • If you only need compact native inference, evaluate TensorFlow Lite instead of the full runtime.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.