3. Support for TensorFlow 2

In TensorFlow version 2, eager mode is enabled by default and Keras is the main API for constructing models. Distribution strategies are the new way of targeting different pieces of hardware.

3.1. IPUStrategy

The tf.distribute.Strategy is an API to distribute training across multiple devices. IPUStrategy is a subclass which targets a single system with one or more IPUs attached. Another subclass, IPUMultiWorkerStrategyV1, targets a distributed system with multiple machines (workers). For more information, see the distributed training section.

Use the strategy.scope() context to ensure that everything within that context will be compiled for the IPU device. You should do this instead of using the tf.device context:

from tensorflow.python import ipu

# Create an IPU distribution strategy
strategy = ipu.ipu_strategy.IPUStrategy()

with strategy.scope():
    ...

Note

It is important to construct a Keras model within the scope of the IPUStrategy, because Keras may create some parts of the model at construction time, and some other parts at execution time.

See the TensorFlow documentation for more details on distribution strategies: https://www.tensorflow.org/guide/distributed_training

3.2. Execution modes

TensorFlow operations can be executed in either graph mode or eager mode. Both of these modes are supported on IPUs, however graph mode is much more efficient. It is therefore important to understand the difference between them and understand how to write TensorFlow programs which will fully utilize the IPU devices.

3.2.1. Graph mode with @tf.function

The TensorFlow function annotation @tf.function converts the body of the annotated function into a fused set of operations that are executed as a group, in the same way as a whole graph would have been in TensorFlow version 1. In addition, a library called autograph will convert Python control flow constructs into TensorFlow graph operations.

It is best practice to ensure that anything which is intended to be executed on the IPU is placed into a Python function which is annotated with @tf.function(experimental_compile=True). Note that this does not apply to constructing a Keras model or using the Keras Model.fit() API. See the Keras with IPUs section for details on Keras.

When calling a function which is marked with a @tf.function(experimental_compile=True) annotation from within a distribution strategy such as IPUStrategy, you should not call it directly, but instead use the run method. For example:

 1import tensorflow as tf
 2from tensorflow.python import ipu
 3
 4# Configure the IPU device.
 5config = ipu.config.IPUConfig()
 6config.auto_select_ipus = 1
 7config.configure_ipu_system()
 8
 9a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
10b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
11
12
13@tf.function(experimental_compile=True)
14def matmul_fn(x, y):
15  z = tf.matmul(x, y)
16  return z
17
18
19strategy = ipu.ipu_strategy.IPUStrategy()
20with strategy.scope():
21  c = strategy.run(matmul_fn, args=(a, b))
22print(c)

Note

When using the @tf.function annotation, it is important to set the experimental_compile=True argument to ensure best performance.

For more information about tf.function and examples, see the TensorFlow documentation at https://www.tensorflow.org/guide/function.

3.2.2. Eager mode

Eager mode is the default execution mode for TensorFlow operations. This mode is supported on IPUs, however it is not as performant as graph mode and we do not recommend using it.

For example, the code below executes the tf.matmul immediately on an IPU device and returns a tf.Tensor object containing the result:

 1import tensorflow as tf
 2from tensorflow.python import ipu
 3
 4# Configure the IPU device.
 5config = ipu.config.IPUConfig()
 6config.auto_select_ipus = 1
 7config.configure_ipu_system()
 8
 9a = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
10b = tf.constant([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
11with tf.device('/device:IPU:0'):
12  c = tf.matmul(a, b)
13
14print(c)

3.3. On-device loops

In the Keras with IPUs section, we describe how to use Keras to perform training, testing and prediction. However, sometimes a more sophisticated loop is required. You can create these to train, test and run inference of your models using a loop created inside of a tf.function - this is commonly known as an on-device loop.

By executing multiple steps of the model with an on-device loop, you can improve the performance of your model. This is achieved by creating a for loop using tf.range inside tf.function; AutoGraph will convert this to a tf.while_loop for you.

For example, the code below creates a custom training loop using an on-device loop to train a simple model:

 1import tensorflow as tf
 2from tensorflow.python import ipu
 3
 4# Configure the IPU device.
 5config = ipu.config.IPUConfig()
 6config.auto_select_ipus = 1
 7config.configure_ipu_system()
 8
 9
10# Create a simple model.
11def create_model():
12  return tf.keras.Sequential([
13      tf.keras.layers.Flatten(),
14      tf.keras.layers.Dense(256, activation='relu'),
15      tf.keras.layers.Dense(128, activation='relu'),
16      tf.keras.layers.Dense(10)
17  ])
18
19
20# Create a dataset for the model.
21def create_dataset():
22  mnist = tf.keras.datasets.mnist
23
24  (x_train, y_train), (_, _) = mnist.load_data()
25  x_train = x_train / 255.0
26
27  train_ds = tf.data.Dataset.from_tensor_slices(
28      (x_train, y_train)).shuffle(10000).batch(32, drop_remainder=True)
29  train_ds = train_ds.map(lambda d, l:
30                          (tf.cast(d, tf.float32), tf.cast(l, tf.int32)))
31
32  return train_ds.repeat().prefetch(16)
33
34
35# Define a function which performs a single training step of a model.
36def training_step(features, labels, model, optimizer):
37  # Execute the model and calculate the loss.
38  with tf.GradientTape() as tape:
39    predictions = model(features, training=True)
40    prediction_loss = tf.keras.losses.sparse_categorical_crossentropy(
41        labels, predictions)
42    loss = tf.reduce_mean(prediction_loss)
43
44  # Apply the gradients.
45  grads = tape.gradient(loss, model.trainable_variables)
46  optimizer.apply_gradients(zip(grads, model.trainable_variables))
47  return loss
48
49
50# Create a loop which performs ``steps_per_execution`` iterations of
51# ``training_step`` every time this function is executed.
52@tf.function(experimental_compile=True)
53def training_loop(iterator, steps_per_execution, outfeed, model, optimizer):
54  # Create an on device loop.
55  for _ in tf.range(steps_per_execution):
56    # Get the next input.
57    features, labels = next(iterator)
58
59    # Perform the training step.
60    loss = training_step(features, labels, model, optimizer)
61
62    # Enqueue the loss after each step to the outfeed queue. This is then read
63    # back on the host for monitoring the model performance.
64    outfeed.enqueue(loss)
65
66
67# Create a strategy for execution on the IPU.
68strategy = ipu.ipu_strategy.IPUStrategy()
69with strategy.scope():
70  # Create a Keras model.
71  model = create_model()
72
73  # Create an optimizer.
74  opt = tf.keras.optimizers.SGD(0.01)
75
76  # Create an iterator inside the strategy for the dataset the model will be
77  # trained on.
78  iterator = iter(create_dataset())
79
80  # Create an IPUOutfeedQueue to collect results from each step.
81  outfeed_queue = ipu.ipu_outfeed_queue.IPUOutfeedQueue()
82
83  # Total number of steps (batches) to run.
84  total_steps = 100
85
86  # How many steps (batches) to execute each time the device executes.
87  steps_per_execution = 10
88
89  for begin_step in range(0, total_steps, steps_per_execution):
90    # Run the training loop.
91    strategy.run(training_loop,
92                 args=(iterator, steps_per_execution, outfeed_queue, model,
93                       opt))
94    # Calculate the mean loss.
95    mean_loss = sum(outfeed_queue) / steps_per_execution
96    print(f"Current step: {begin_step}, training loss: {mean_loss}")