TensorFlow
while_loop
non-deterministic
machine learning
programming

Non-deterministic behavior of TensorFlow while_loop

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

tf.while_loop can exhibit non-deterministic behavior due to parallel execution of loop iterations, GPU thread scheduling, floating-point operation ordering, and XLA optimizations. This means running the same code twice can produce slightly different numerical results. The non-determinism primarily affects operations inside the loop body that use parallel reductions (like tf.reduce_sum on GPU) or access shared state. To get deterministic results, set tf.config.experimental.enable_op_determinism(), use parallel_iterations=1, or switch to CPU execution.

Understanding tf.while_loop

python
1import tensorflow as tf
2
3# Basic while_loop: sum numbers from 0 to 9
4def condition(i, total):
5    return i < 10
6
7def body(i, total):
8    return i + 1, total + i
9
10i0 = tf.constant(0)
11total0 = tf.constant(0)
12
13final_i, final_total = tf.while_loop(condition, body, [i0, total0])
14print(final_total.numpy())  # 45

tf.while_loop takes a condition function, a body function, and loop variables. It repeatedly executes the body while the condition returns True. Unlike Python while, it operates on TensorFlow tensors and supports automatic differentiation.

Sources of Non-Determinism

python
1import tensorflow as tf
2
3# Source 1: parallel_iterations (default=10)
4# Multiple loop iterations can execute concurrently on GPU
5def body(i, total):
6    # This reduction is non-deterministic on GPU
7    values = tf.random.uniform([1000])
8    return i + 1, total + tf.reduce_sum(values)
9
10# Source 2: GPU floating-point atomics
11# tf.reduce_sum on GPU uses atomic adds — order varies between runs
12data = tf.random.uniform([10000])
13sum1 = tf.reduce_sum(data)  # May differ slightly each run on GPU
14
15# Source 3: XLA compilation may reorder operations
16@tf.function(jit_compile=True)
17def xla_loop():
18    return tf.while_loop(condition, body, [tf.constant(0), tf.constant(0.0)])

The parallel_iterations parameter (default 10) allows TensorFlow to execute multiple loop iterations simultaneously. When these iterations share accumulation variables or use non-associative floating-point operations, the order of additions varies between runs.

Demonstrating Non-Determinism

python
1import tensorflow as tf
2import numpy as np
3
4@tf.function
5def sum_with_while_loop(data):
6    def cond(i, total):
7        return i < tf.shape(data)[0]
8
9    def body(i, total):
10        return i + 1, total + data[i]
11
12    _, result = tf.while_loop(cond, body, [0, 0.0])
13    return result
14
15data = tf.constant(np.random.randn(10000).astype(np.float32))
16
17# Run multiple times — results may differ slightly on GPU
18results = [sum_with_while_loop(data).numpy() for _ in range(5)]
19print(results)
20# Possible output (differences in last decimal places):
21# [-12.345679, -12.345680, -12.345679, -12.345681, -12.345679]

The differences are small (last few decimal places) but can compound over many iterations, especially in gradient computations during training.

Fix 1: Set parallel_iterations=1

python
1import tensorflow as tf
2
3def condition(i, total):
4    return i < 100
5
6def body(i, total):
7    return i + 1, total + tf.cast(i, tf.float32)
8
9# Deterministic: only one iteration runs at a time
10_, result = tf.while_loop(
11    condition, body,
12    [tf.constant(0), tf.constant(0.0)],
13    parallel_iterations=1  # Sequential execution
14)
15print(result.numpy())  # Always the same

Setting parallel_iterations=1 forces sequential execution, eliminating ordering non-determinism within the loop. This slows execution but guarantees reproducibility.

Fix 2: Enable Global Determinism

python
1import tensorflow as tf
2
3# TensorFlow 2.9+: enable deterministic ops globally
4tf.config.experimental.enable_op_determinism()
5
6# Now all ops, including those inside while_loop, are deterministic
7# This may reduce performance (disables GPU parallelism for some ops)
8
9# Verify
10tf.debugging.assert_equal(
11    tf.reduce_sum(tf.ones([10000])),
12    tf.reduce_sum(tf.ones([10000]))
13)

enable_op_determinism() forces all TensorFlow operations to produce deterministic results. This affects the entire session and may slow down GPU operations that normally use non-deterministic parallel reductions.

Fix 3: Set Random Seeds

python
1import tensorflow as tf
2import numpy as np
3import random
4
5# Set all random seeds for full reproducibility
6def set_seeds(seed=42):
7    tf.random.set_seed(seed)
8    np.random.seed(seed)
9    random.seed(seed)
10
11set_seeds(42)
12
13# Random operations inside while_loop are now deterministic
14def body(i, total):
15    return i + 1, total + tf.random.uniform([1])[0]
16
17_, result = tf.while_loop(
18    lambda i, t: i < 10,
19    body,
20    [tf.constant(0), tf.constant(0.0)],
21    parallel_iterations=1
22)
23print(result.numpy())  # Same every run with same seed

Setting tf.random.set_seed makes random number generation deterministic. Combined with parallel_iterations=1, this ensures the random values are generated in the same order every run.

Non-Determinism in Training Loops

python
1import tensorflow as tf
2
3# Training with while_loop-based RNNs can be non-deterministic
4model = tf.keras.Sequential([
5    tf.keras.layers.LSTM(64),  # LSTM uses while_loop internally
6    tf.keras.layers.Dense(1)
7])
8
9# Make training deterministic
10tf.config.experimental.enable_op_determinism()
11tf.random.set_seed(42)
12
13model.compile(optimizer='adam', loss='mse')
14model.fit(X_train, y_train, epochs=5)
15# Results are now reproducible across runs

RNN layers (LSTM, GRU) use tf.while_loop internally to iterate over time steps. Non-determinism in the loop affects gradient computation, causing different training outcomes across runs.

shape_invariants for Dynamic Shapes

python
1import tensorflow as tf
2
3# while_loop with accumulating tensor (dynamic shape)
4def cond(i, arr):
5    return i < 5
6
7def body(i, arr):
8    new_val = tf.expand_dims(tf.cast(i * 2, tf.float32), 0)
9    arr = tf.concat([arr, new_val], axis=0)
10    return i + 1, arr
11
12_, result = tf.while_loop(
13    cond, body,
14    [tf.constant(0), tf.constant([], dtype=tf.float32)],
15    shape_invariants=[
16        tf.TensorShape([]),
17        tf.TensorShape([None])  # Dynamic first dimension
18    ]
19)
20print(result.numpy())  # [0. 2. 4. 6. 8.]

When loop variables change shape across iterations, use shape_invariants to declare which dimensions are dynamic. Without this, TensorFlow raises a shape mismatch error.

Common Pitfalls

  • Assuming while_loop is sequential by default: parallel_iterations defaults to 10, meaning up to 10 iterations can run concurrently. This improves performance but introduces non-determinism when iterations modify shared accumulators. Set parallel_iterations=1 for deterministic behavior.
  • Floating-point ordering on GPU: tf.reduce_sum on GPU uses atomic additions that execute in non-deterministic order. Due to floating-point non-associativity, (a + b) + c may differ from a + (b + c). Use enable_op_determinism() or run on CPU for exact reproducibility.
  • Not setting seeds for random ops in loop: tf.random functions inside tf.while_loop without a global seed produce different sequences each run. Set tf.random.set_seed() before the loop and use parallel_iterations=1 to ensure consistent random number generation order.
  • Forgetting shape_invariants for dynamic tensors: If a loop variable changes shape (e.g., concatenating to a growing tensor), tf.while_loop raises an error unless shape_invariants declares the variable dimensions. Use tf.TensorShape([None]) for dimensions that grow.
  • Gradients amplify non-determinism: Small numerical differences in the forward pass become larger differences in gradients during backpropagation. A model trained with non-deterministic while_loop may converge to different solutions across runs, making debugging and benchmarking unreliable.

Summary

  • tf.while_loop can produce non-deterministic results due to parallel iteration execution and GPU floating-point ordering
  • Set parallel_iterations=1 to force sequential execution within the loop
  • Use tf.config.experimental.enable_op_determinism() for global determinism (TF 2.9+)
  • Set tf.random.set_seed() to make random operations inside loops reproducible
  • RNN layers (LSTM, GRU) use while_loop internally and are affected by the same non-determinism
  • Use shape_invariants when loop variables change shape across iterations

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free 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