TensorFlow
while_loop
non-deterministic
machine learning
programming

Non-deterministic behavior of TensorFlow while_loop

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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

Course illustration
Course illustration

All Rights Reserved.