When fit() isn't enough
Sometimes model.fit() isn't flexible enough — GANs need interleaved generator/discriminator updates, multi-task losses with custom weighting, gradient surgery for research. Custom training loops with GradientTape give you full control.
Always decorate your train_step with @tf.function outside the loop. The first call traces and compiles the graph; subsequent calls run the compiled version directly. This is what gives custom loops parity with fit()'s speed.
Keras metrics accumulate. Calling metric.result() returns the running average since the last reset_state(). If you forget to reset between epochs, your epoch-2 metrics include epoch-1 data and you'll wonder why numbers look weird. Reset after every epoch summary.