InterviewsVector

Fix Keras model.fit Failing Under MirroredStrategy (Multi-GPU)

Quick answer

model.fit failing under tf.distribute.MirroredStrategy is usually one of three things: the model and optimizer were created OUTSIDE strategy.scope() (they must be built inside it), the global batch size wasn't scaled for the replicas (fit splits the global batch across GPUs, so use per_replica_batch × num_replicas), or a custom train_step/metric isn't replica-aware. Build everything inside strategy.scope(), feed a dataset batched at the global size, and let model.fit handle the distribution.

Short answer: model.fit failing under MirroredStrategy is nearly always one of three things: the model/optimizer built outside strategy.scope(), a global batch size that isn't scaled for the replicas, or a non-replica-aware custom step. Build everything inside the scope and let fit distribute it.

tf.distribute.MirroredStrategy runs data-parallel training across the GPUs on one machine. With Keras, model.fit handles the distribution for you — if you set it up correctly.

The correct pattern

Create and compile the model inside strategy.scope() so its variables are mirrored:

import tensorflow as tf
 
strategy = tf.distribute.MirroredStrategy()
print("Replicas:", strategy.num_replicas_in_sync)
 
with strategy.scope():
    model = create_model()          # build here
    model.compile(                  # compile here
        optimizer="adam",
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )
 
# fit OUTSIDE the scope is fine — the model is already distributed
model.fit(train_ds, epochs=10)

The three usual failures

1. Model/optimizer built outside the scope. Distributed (mirrored) variables must be created inside strategy.scope(). If you build the model first and only wrap fit, you'll get errors about variables not belonging to the strategy. Fix: move construction and compile inside the scope.

2. Global batch size not scaled. fit splits the global batch across replicas. With 2 GPUs, a global batch of 64 runs 32 per GPU. Scale the global batch up with the replica count so each GPU stays well-fed:

per_replica = 32
global_batch = per_replica * strategy.num_replicas_in_sync
train_ds = train_ds.batch(global_batch)

(And typically scale the learning rate with the global batch.)

3. A non-replica-aware custom step. If you wrote a custom train_step or metric, it must reduce correctly across replicas. For standard Keras training you don't need this — plain model.fit is already replica-aware.

Don't "test" with strategy.run(model, ...)

A common wrong fix. strategy.run executes a per-replica step function, not a model:

# ❌ wrong — strategy.run takes a function, not a model
strategy.run(model, args=())
 
# ✅ if you really need a custom loop:
@tf.function
def train_step(inputs):
    x, y = inputs
    with tf.GradientTape() as tape:
        loss = compute_loss(y, model(x, training=True))
    grads = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))
    return loss
 
strategy.run(train_step, args=(next(iter(dist_ds)),))

Common traps

  • OOM under MirroredStrategy — the per-replica batch (global ÷ GPUs) is still too large.
  • Metrics look wrong — a custom metric that isn't reduced across replicas.
  • Building the model outside the scope — the single most frequent cause.

Sources

Key takeaways

  • Create the model AND optimizer inside `with strategy.scope():` — this is the #1 cause of failures.
  • fit() splits the GLOBAL batch across replicas; set global batch = per_replica × num_replicas.
  • Don't call strategy.run(model, ...) to 'test' — strategy.run takes a per-replica step function, not a model.
  • model.fit is already distribution-aware; you rarely need strategy.run yourself.
  • OOM under MirroredStrategy usually means the per-replica batch (global ÷ GPUs) is still too big.

Frequently asked questions

Why does my model fail only under MirroredStrategy?

Almost always because the model or optimizer was created outside strategy.scope(). Distributed variables must be created inside the scope so they're mirrored across replicas. Move model construction and compilation inside `with strategy.scope():` and re-run.

How should I set the batch size with MirroredStrategy?

Think in terms of the global batch. model.fit splits the global batch evenly across replicas, so with 2 GPUs a global batch of 64 runs 32 per GPU. Scale the global batch up with the replica count (per_replica × num_replicas), and often scale the learning rate accordingly.

Do I need strategy.run() with Keras?

No. model.fit(), evaluate(), and predict() are already distribution-aware when called inside a strategy. strategy.run() is for CUSTOM training loops, and it takes a per-replica step function (not a model). Passing a model to strategy.run is a mistake.

By Mohammad Wasi

Software Engineering Leader & Technical Author · Updated August 26, 2026


Related Posts