skip to content

How do you set and inspect the serving_default signature of a SavedModel?

level: middleimportance: must knowfreq 64%

answer

  1. The callable surface, not the graph
  2. One key, named inputs, named outputs
  3. serving_default is the fallback key
  4. signatures= takes concrete functions
  5. saved_model_cli show --all

basics

~20 s

Pass signatures= to tf.saved_model.save() mapping a key such as serving_default to a concrete function, or let Keras 3's model.export() create it. Inspect with saved_model_cli show --dir m/1 --all, or load the model and read .signatures.

solid answer

~40 s

A signature is the **named callable surface** a SavedModel exposes: a key, a set of named input tensors with dtypes and shapes, and a set of named outputs. TF Serving calls the key `serving_default` when a request does not name one — the constant is `tf.saved_model.DEFAULT_SERVING_SIGNATURE_DEF_KEY`. You set it either implicitly (Keras 3's `model.export(path)` derives `serving_default` from the model's call) or explicitly, by passing `signatures={"serving_default": fn.get_concrete_function(...)}` to `tf.saved_model.save()`. You can register several keys — for example a raw-bytes endpoint and a preprocessed-tensor endpoint — and clients select one via `model_spec.signature_name` in gRPC. Output names come from the returned Python dict's keys; return a bare tensor and you get an autogenerated name like `output_0`, which is awkward for callers. Inspect anything with `saved_model_cli show --dir m/1 --all`.

go deeper

for a junior

Know that a SavedModel exposes named entry points called signatures, that the usual one is serving_default, and that saved_model_cli show --all prints them.

for a middle

Be ready to write the export call yourself: build a concrete function, register it under a key, return a dict so outputs are named, then inspect the result. Explain what a SignatureDef records.

for a senior

Show that you treat the signature as a published contract — multiple endpoints for raw versus preprocessed input, stable output names across versions, and a local saved_model_cli run as a pre-deploy check.

for a principal

Own signature governance across teams: who may change an input or output name, how a breaking signature change is versioned and rolled out to clients, and whether preprocessing belongs inside the graph endpoint or in a gateway.

## What a signature is When a SavedModel is loaded by a runtime that cannot see your Python, the runtime needs to know *what it may call*. That list is the signature map: a dictionary from string key to a `SignatureDef` describing a single entry point — its input tensors (name, dtype, shape) and its output tensors (name, dtype, shape). Anything not in that map is unreachable from a server, even if the code is present in the graph. A model can be loaded successfully and still be unusable because it exposes no signature. ## Setting it explicitly The explicit path is a keyword argument on the save call: `tf.saved_model.save(obj, "m/1", signatures={"serving_default": obj.predict.get_concrete_function(tf.TensorSpec([None, 10], tf.float32))})` Two details matter. First, the value must be a **concrete function** — a `tf.function` traced against specific input specs — because a `tf.function` alone is polymorphic and has no single input contract. If the `tf.function` was declared with `input_signature=[...]`, passing it directly works because it already has exactly one concrete form. Second, the key is an ordinary string; `serving_default` is only special in that servers fall back to it. ## What Keras gives you In Keras 3, `model.export("m/1")` builds the SavedModel and derives a `serving_default` from the model's forward pass, using the model's known input spec. When you need more control — several endpoints, custom preprocessing, or a different input spec than the model was built with — use `keras.export.ExportArchive`: `track()` the model, `add_endpoint(name=..., fn=..., input_signature=[...])` once per endpoint, then `write_out(path)`. ## Naming the outputs Output names in the `SignatureDef` come from what your function returns. Return `{"probability": p, "label": l}` and clients see those keys in the REST response and in gRPC's `outputs` map. Return a bare tensor and TensorFlow invents a name such as `output_0`. Returning a dict is worth the extra line: it makes the response self-describing and lets you add an output later without renaming the existing one. Input names work the same way: the parameter names of the exported function (or the `name=` on each `tf.TensorSpec`) become the keys clients use in a columnar request. ## Multiple signatures A single SavedModel can expose several endpoints, and this is the clean way to serve two shapes of the same model — for example one endpoint that takes an already-decoded float tensor and one that takes raw JPEG bytes and does the decode inside the graph. Clients pick with `signature_name` over gRPC. The REST predict endpoint uses `serving_default` unless the request supplies `signature_name` in the body, so make the endpoint most clients want the default one. ## Inspecting The command-line tool ships with TensorFlow: - `saved_model_cli show --dir m/1` lists tag sets. - `saved_model_cli show --dir m/1 --tag_set serve` lists signature keys. - `saved_model_cli show --dir m/1 --all` prints every signature with each input and output's name, dtype and shape. - `saved_model_cli run --dir m/1 --tag_set serve --signature_def serving_default --input_exprs '...'` actually executes it locally, which is the fastest way to prove the artifact works before a server is involved. From Python, `tf.saved_model.load("m/1")` returns an object whose `.signatures` is a dict of concrete functions; `list(loaded.signatures)` gives the keys, and each function exposes `structured_input_signature` and `structured_outputs`. ## Gotchas - **An empty signature map.** Saving a plain `tf.Module` whose methods are not `tf.function`s, or forgetting the `signatures=` argument for a non-Keras object, yields a SavedModel with nothing callable from a server. `--all` shows this instantly. - **Assuming the Python method name is the key.** It is not; the key is whatever string you registered, and servers look for `serving_default`. - **Shapes pinned by tracing.** The concrete function you register fixes the accepted shapes, so trace with `None` where you need a variable batch. - **Signature drift between versions.** Renaming an output key is a breaking change for every client, even though the model still loads. Treat the signature as a published API contract.

  • Why must the signatures= argument receive a concrete function rather than a plain tf.function?
    A `tf.function` is polymorphic: it can be traced for many input dtype/shape combinations, so it has no single input contract to write into a `SignatureDef`. `get_concrete_function(tf.TensorSpec(...))` picks exactly one, giving fixed input and output specs. A `tf.function` declared with `input_signature=[...]` can be passed directly because it already admits only one form.
  • How does a client select a non-default signature at request time?
    Over gRPC, set `request.model_spec.signature_name` on the `PredictRequest` to the registered key. Over the REST predict endpoint, include `"signature_name"` alongside `instances` or `inputs` in the JSON body. Omit it and the server uses `serving_default`; naming a key that was never registered returns an error rather than falling back.
  • Your served model returns an output named output_0 and the client team wants a meaningful name. What changed in the export?
    The exported function returned a bare tensor, so TensorFlow autogenerated the name. Return a dict instead — `return {"score": s}` — and re-export; the key becomes the output name in both the REST response and the gRPC `outputs` map. Nothing about the model itself needs to change, only the return shape of the exported function.

saying these in an interview costs you the question

  • Thinking serving_default is created automatically for any object
  • Confusing the tag set 'serve' with the signature key
  • Passing an untraced tf.function to signatures=
  • Assuming output names come from Python variable names
  • Renaming signature outputs and calling it a non-breaking change

context