Debugging, profiling and export formats
Shape and dtype errors, NaN losses, the tf.debugging toolkit, the profiler, and exporting to TF Lite, TF.js and TF Serving from one model.
The errors you will actually hit
| Error | Root cause | Fix |
|---|---|---|
Shapes ... are incompatible | Mismatched inner dimensions | Print x.shape at every stage; match the last axis of A to the second of B |
Could not find matching concrete function | Unseen input signature for a tf.function | Pass a TensorSpec or vary the input shape explicitly |
Loss is nan | Exploding gradients, log(0), bad learning rate | Clip gradients, use from_logits=True, lower the rate |
Accuracy stuck at 1/num_classes | Labels shuffled versus logits, or a softmax applied twice | Align target dtype and shape; check from_logits |
| Out of memory at the end of an epoch | Validation batch larger than the training batch | Set the same batch size, or use a smaller validation batch |
| Slow first call only | Graph tracing | Warm up with a representative batch before benchmarking |
import tensorflow as tf
# asserts that run in the graph and raise with a readable message
tf.debugging.assert_shapes([
(x, ("batch", "features")),
(y, ("batch",)),
])
@tf.function
def train_step(x, y):
with tf.GradientTape() as tape:
logits = model(x, training=True)
tf.debugging.assert_all_finite(logits, "non-finite logits")
loss = loss_fn(y, logits)
tf.debugging.assert_all_finite(loss, "non-finite loss")
grads = tape.gradient(loss, model.trainable_variables)
# check_numerics surfaces the offending op instead of letting NaN spread
grads = [tf.debugging.check_numerics(g, f"grad {v.name}")
for g, v in zip(grads, model.trainable_variables)]
optimiser.apply_gradients(zip(grads, model.trainable_variables))
return loss- Debug in eager mode first (
model(x)outside atf.function). Eager errors point at the exact line, graph errors point at a node name. - Use
tf.debugging.enable_check_numerics()to make the first non-finite value raise at its source rather than silently propagate to the loss. - Run the model on a single tiny batch and try to overfit it to zero loss. If that fails, the bug is structural; if it succeeds, the bug is in the data or the schedule.
Profiling a training step
# the profiler records a short window; open the result in TensorBoard
tf.profiler.experimental.start("logs/profile")
for step, (x, y) in enumerate(train_ds.take(20)):
with tf.profiler.experimental.Trace("train", step_num=step):
loss = train_step(x, y)
if step == 10:
break
tf.profiler.experimental.stop() # then: tensorboard --logdir logs/profile- Read the Trace Viewer top-down: the largest bar is the bottleneck. If the input pipeline bar is empty much of the time, you are input-bound.
- Profile short windows and warm up first. A profile of the first ten steps measures graph construction, not steady-state training.
- Compare kernel time against total step time. A large gap means the accelerator is waiting, and the fix is almost always in the input pipeline.
- Turn off profiling for the real run: it adds overhead and produces large files.
Export formats and their trade-offs
import tensorflow as tf
# 1. Keras format: the full model with preprocessing layers, for resuming work
model.save("model.keras")
# 2. SavedModel: the serving format, with a fixed inference signature
class ServingModule(tf.Module):
def __init__(self, model):
self.model = model
@tf.function(input_signature=[
tf.TensorSpec(shape=[None, 224, 224, 3], dtype=tf.float32, name="image")])
def serve(self, image):
return {"scores": tf.nn.softmax(self.model(image, training=False))}
module = ServingModule(model)
tf.saved_model.save(module, "saved_model",
signatures={"serving_default": module.serve})
# 3. TF Lite: quantised, for mobile and edge
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT] # int8 post-training quantisation
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
tflite_model = converter.convert()
open("model.tflite", "wb").write(tflite_model)| Format | Runs where | Size | Comment |
|---|---|---|---|
.keras | Python, TensorFlow | Largest | Keeps the optimiser state and custom layers |
| SavedModel | TF Serving, TF.js, Python | Large | Standard serving format, fixed signatures |
| TFLite | Mobile, microcontrollers, edge | Smallest | Quantisation costs a little accuracy |
| TF.js Layers | Browser via tfjs-converter | Medium | Test in the target browser, not in Python |
⚠️
Always run a parity check after exporting: feed the same batch to the original model and the exported artefact and compare outputs. Quantisation, op fallbacks and shape fixing all produce small differences, and an unverified export is the least visible bug in the whole pipeline.
FAQ
How do I reproduce a NaN loss?
Lower the learning rate by 10x. If the NaN disappears, it was divergence. If it remains, enable
tf.debugging.enable_check_numerics() to find the op that produces the first non-finite value.Why is my exported model less accurate?
Usually quantisation, or a preprocessing difference: the export may run in a different dtype, or your inference path skips a step that the in-model preprocessing used to do. Compare on identical inputs with the sigmoid and thresholding held fixed.
Related
TensorBoard and experiment tracking Mixed precision and multi-device training
Last refreshed 2026-09-18.