Skip to content

Apache MXNet - Distributed Training

This chapter covers distributed training with Apache MXNet, enabling the use of multiple computational resources (CPUs/GPUs across one or more machines) to accelerate and scale model training.

MXNet supports two primary programming paradigms:

This mode, primarily exposed through the Gluon API, allows for defining and executing computations step-by-step, much like standard Python code or NumPy. Operations are typically executed immediately.

import mxnet as mx
from mxnet import nd
# Create tensors on CPU and GPU (if available)
tensor_cpu = nd.zeros((100,), ctx=mx.cpu())
# tensor_gpu = nd.zeros((100,), ctx=mx.gpu(0)) # Uncomment if a GPU is present
# print(tensor_cpu)
# if 'tensor_gpu' in locals(): print(tensor_gpu)

While imperative, MXNet’s backend execution engine can still optimize and parallelize operations, often deferring actual computation until results are needed (lazy evaluation for some operations).

In symbolic mode, you first define the computation graph using mx.symbol objects. This graph represents the operations and their dependencies but doesn’t execute them immediately. The graph is then compiled and bound with data to be executed efficiently.

import mxnet as mx
from mxnet import sym
x_sym = sym.Variable("data_x")
y_sym = sym.Variable("data_y")
z_sym = (x_sym + y_sym) / 100
# To execute, this symbol would be bound to data using a Module or Executor
# executor = z_sym.simple_bind(ctx=mx.cpu(), data_x=(10,1), data_y=(10,1))
# For Gluon users, symbolic programming is often abstracted by HybridBlocks.

Gluon’s HybridBlock allows models to be defined imperatively and then optionally converted (hybridized) into a symbolic graph for performance benefits, blending the flexibility of imperative programming with the speed of symbolic execution.

MXNet supports different strategies to distribute the workload of training a neural network:

This is the most common strategy. The model is replicated on multiple devices (e.g., GPUs). Each device processes a different mini-batch of the training data. Gradients computed on each device are then aggregated (e.g., averaged), and the model parameters are updated synchronously or asynchronously. This can be done on a single machine with multiple GPUs or across multiple machines.

Used when a model is too large to fit into a single device’s memory. Different parts of the model are placed on different devices. Data flows through these parts sequentially or in a more complex pipeline. MXNet’s support for model parallelism is typically focused on single-machine scenarios, though complex setups can extend this.

Core Components of MXNet Distributed Training

Section titled “Core Components of MXNet Distributed Training”

Understanding the following components is key to grasping MXNet’s distributed training architecture:

In a typical MXNet distributed setup, processes take on specific roles:

  • Worker: Performs the actual training computations. Each worker typically handles a portion of the data, computes gradients, sends them to parameter servers, and receives updated parameters.
  • Server (Parameter Server): Stores and manages the model’s parameters. It receives gradients from workers, aggregates them, updates the parameters (or provides gradients to workers for local updates), and sends updated parameters back to workers. Multiple servers can be used to distribute the parameter storage and update load.
  • Scheduler: Manages the setup and coordination of the cluster. It ensures all nodes (workers and servers) can find and communicate with each other. There is typically one scheduler in the cluster.

The KVStore is the backbone for communication in distributed training, especially for data parallelism. It manages the storage and synchronization of parameters (keys are parameter names/indices, values are the NDArrays of parameters).

  • Workers push gradients to the KVStore.
  • Workers pull updated parameters from the KVStore.
  • The KVStore handles aggregation of gradients and updates to parameters, either on the server-side or by providing components for client-side updates.

To enable distributed training, a distributed KVStore is created:

# Example: Creating a distributed synchronous KVStore
# This code would be run as part of a script launched by tools/launch.py or similar
# kv_type = 'dist_sync' # or 'dist_async', etc.
# kv = mx.kv.create(kv_type)
# print(f"Created KVStore of type: {kv_type}")
print("To use distributed KVStore, e.g., mx.kv.create('dist_sync'), environment variables like DMLC_ROLE are needed.")

In a multi-server setup, parameters (keys) are distributed across the servers. The KVStore client library transparently routes requests for a specific key to the correct server. Large parameter arrays might even be sharded across multiple servers.

For effective data parallelism, each worker should process a unique subset of the overall training dataset. MXNet’s data iterators like mxnet.io.MNISTIterator and mxnet.gluon.data.DataLoader can be configured for distributed training, ensuring each worker gets its designated shard. The num_workers and rank (or similar) parameters in data loading utilities help in partitioning the data correctly.

Within a single worker (which might itself have multiple GPUs), mxnet.gluon.utils.split_and_load can further divide a mini-batch for processing on local devices.

The gluon.Trainer object, when used with a distributed KVStore, manages parameter updates. The update_on_kvstore parameter in Trainer is crucial:

  • If update_on_kvstore=True (or not set, often defaults to True for distributed KVStores), the optimizer’s update logic is executed on the parameter servers. Workers push gradients, servers update parameters, workers pull updated parameters.
  • If update_on_kvstore=False, workers push gradients, servers aggregate them and send aggregated gradients back. Workers then perform local parameter updates using these aggregated gradients. This can sometimes reduce communication overhead or allow for more complex local update rules.
from mxnet import gluon
# net = gluon.nn.Sequential() # ... define your network ...
# net.initialize(ctx=contexts) # contexts could be [mx.gpu(0), mx.gpu(1)]
# kv = mx.kv.create('dist_sync') # Assuming this is a distributed worker node
# trainer = gluon.Trainer(
# net.collect_params(),
# optimizer='sgd',
# optimizer_params={'learning_rate': 0.01},
# kvstore=kv,
# update_on_kvstore=True # or False, depending on desired strategy
# )
print("gluon.Trainer's update_on_kvstore parameter controls where parameter updates occur.")

The string argument to mx.kv.create() determines the type and behavior of the distributed KVStore:

All workers operate synchronously. After processing a batch, each worker pushes its gradients. The server(s) wait to receive gradients from all (or a configured number of) workers before aggregating them and updating the parameters. All workers then pull the same, newly updated parameters before starting the next batch.

Pros: Simpler to reason about, often leads to more stable convergence. Cons: Can be slow if some workers are significantly slower than others (straggler problem). A crashed worker can halt progress.

Workers operate asynchronously. When a worker finishes a batch and pushes gradients, the server updates its parameters immediately without waiting for other workers. The worker then pulls the latest available parameters, which might have been updated using gradients from other workers that finished earlier or later.

Pros: Can lead to higher throughput as faster workers don’t wait for slower ones. More resilient to worker failures. Cons: Gradients can be ‘stale’ (computed based on older parameter versions), potentially leading to less stable convergence or requiring careful tuning of learning rates.

dist_device_sync (or similar names like dist_sync_device)

Section titled “dist_device_sync (or similar names like dist_sync_device)”

Similar to dist_sync, but optimized for scenarios where each worker node has multiple GPUs. Gradient aggregation and parameter updates are attempted to be performed directly on device memory (GPU) to reduce CPU-GPU data transfer overhead on the server side (if applicable) or on the worker side for local multi-GPU aggregation before communication.

Pros: Potentially faster than dist_sync by minimizing data movement. Cons: May increase GPU memory usage.

dist_device_async (or similar names like dist_async_device)

Section titled “dist_device_async (or similar names like dist_async_device)”

Combines the asynchronous nature of dist_async with the device-memory optimization attempts of dist_device_sync.

Choosing the right distributed mode depends on the specific hardware setup, network conditions, model architecture, and desired trade-offs between training speed, convergence stability, and implementation complexity. For launching distributed MXNet jobs, tools like tools/launch.py (from the MXNet source repository) or integrations with cluster managers (e.g., Kubernetes, YARN) are typically used to set up necessary environment variables (DMLC_ROLE, DMLC_PS_ROOT_URI, etc.) for the scheduler, server, and worker processes.