One Code to Rule Them All: Simplifying AI Development with Hardware-Agnostic Abstraction Layers

Björn Runåker
11 min readMar 25, 2023

--

AI breaking free from HW lock-in

Are you tired of dealing with the complexities of different AI accelerators when developing your applications? Imagine being able to write code once and seamlessly run it on various hardware platforms, such as NVIDIA GPUs or Intel GPUs, without any modifications. In this blog post, we’ll explore how abstraction layers can simplify the development process by providing a high-level API that enables your applications to run on different AI accelerators without changing the code.

We’ll dive into the benefits of using abstraction layers, such as increased portability and reduced hardware-specific knowledge requirements. Additionally, we’ll discuss how these layers work, from high-level API calls to the optimized instructions that communicate with the specific AI accelerators. Finally, we’ll explore examples of popular abstraction frameworks, like OpenCL, CUDA, and OpenXLA, that have made it easier for developers to create high-performance applications that can run effortlessly across various hardware platforms. So, let’s embark on this journey to simplify AI accelerator integration and unlock the true potential of your applications!

Introducing OpenXLA: The Key to Unlocking Hardware Freedom in AI Development

In today’s fast-paced AI landscape, being locked into a software ecosystem can limit your ability to leverage the full potential of AI solutions. OpenXLA is here to change that. This innovative machine learning compiler ecosystem empowers developers to compile and enhance their models from popular ML frameworks, such as PyTorch, TensorFlow, and JAX, leading to improved training and serving capabilities on a diverse range of hardware.

OpenXLA allows developers to optimize their models for high-performance execution across various hardware platforms, including GPUs, CPUs, and ML accelerators. This flexibility allows you to break free from the constraints of a single software ecosystem, enabling you to tap into the true potential of your AI solutions.

While specific examples of OpenXLA-optimized models may not be readily available, any model built using PyTorch, TensorFlow, or JAX can likely benefit from the performance enhancements and hardware versatility provided by OpenXLA. Embrace the power of OpenXLA and experience a new level of freedom in your AI development journey!

As the world of AI and machine learning continues to evolve, developers face the challenge of optimizing their models for various hardware platforms. OpenXLA, a revolutionary open-source machine learning compiler ecosystem, aims to address this challenge by delivering unmatched portability for ML developers. In this blog post, we’ll delve into the key features of OpenXLA and explore how it enhances the portability of ML models across diverse hardware platforms.

OpenXLA’s portability prowess stems from its ability to optimize models from popular ML frameworks, such as PyTorch, TensorFlow, and JAX, for high-performance execution on a wide array of hardware, including GPUs, CPUs, and ML accelerators. This hardware-agnostic approach ensures that developers can maximize the potential of their AI solutions without being locked into a specific ecosystem.

The LLVM compiler infrastructure serves as the backbone of OpenXLA’s optimization capabilities. By leveraging LLVM, OpenXLA efficiently compiles and optimizes models for target hardware, guaranteeing optimal performance and resource utilization. This streamlined process allows developers to seamlessly deploy their ML models on diverse hardware platforms without the need to write platform-specific code.

Moreover, OpenXLA’s consistent interface and abstraction of AI accelerator complexities simplify the development process, enabling developers to focus on their application’s core functionality. This ease of use, coupled with seamless compatibility with leading ML frameworks like PyTorch, TensorFlow, and JAX, ensures that developers can readily integrate OpenXLA into their existing workflows and reap the benefits of its advanced capabilities.

As an open-source, community-driven project, OpenXLA encourages collaboration and contribution from developers worldwide. This collective effort ensures that OpenXLA continues to grow and adapt to the ever-evolving landscape of AI and machine learning, further enhancing its portability features and offering a more flexible and efficient solution for ML developers.

OpenXLA simplifies ML development with a modular toolchain supported by all-powerful frameworks, using a shared compiler interface, standard model representations, and domain-specific compilation with powerful target-independent and hardware-specific optimizations. These tools — XLA, StableHLO, and IREE — all rely on MLIR — a compiler infrastructure optimizing and executing ML models on various hardware.

Exploring MLIR: A Flexible and Extensible Compiler Infrastructure for Machine Learning

As machine learning advances, developers often face performance optimization and hardware support challenges. MLIR (Multi-Level Intermediate Representation), an open-source project within the LLVM ecosystem, is designed to address these challenges by providing a flexible and extensible compiler infrastructure tailored for machine learning workloads. In this blog post, we’ll dive into the key features of MLIR and examine how it’s revolutionizing the world of machine learning and compiler design.

  1. Multi-Level Representation: MLIR is designed to accommodate various levels of abstraction, ranging from high-level domain-specific operations to low-level hardware-specific instructions. This multi-level representation enables seamless integration of domain-specific optimizations and transformations, ultimately improving performance and hardware support.
  2. Modular Infrastructure: MLIR’s modular infrastructure allows developers to easily customize and extend the compiler framework to suit their needs. This modularity fosters innovation and facilitates the development of new optimizations and dialects, paving the way for more efficient and specialized machine-learning systems.
  3. Dialects: MLIR introduces the concept of dialects, which are collections of custom operations tailored for specific domains or hardware platforms. Dialects enable developers to define their operations and transformations, allowing them to target particular hardware accelerators or optimize domain-specific algorithms.
  4. High-Level Optimizations: MLIR’s focus on high-level optimizations ensures that the compiler can efficiently tackle complex machine learning tasks, such as loop fusion, tiling, and data layout transformations. These optimizations improve performance and reduce memory footprint, making MLIR a powerful tool for machine learning developers.
  5. Interoperability: MLIR is designed to work seamlessly with other LLVM-based tools and projects, ensuring smooth integration into existing workflows. This interoperability allows developers to leverage the power of the LLVM ecosystem, further boosting performance and hardware support.

MLIR is an advanced compiler infrastructure that simplifies machine learning development. It offers extensibility, efficiency, and flexibility for tackling complex optimization problems and targeting various hardware platforms. Unlock the potential of your machine learning systems by exploring MLIR and revolutionizing your development process!

IREE: Intermediate Representation Execution Environment

As the field of artificial intelligence (AI) and machine learning (ML) continues to expand, developers are constantly seeking innovative solutions to optimize and streamline their ML workloads. One such solution is IREE (Intermediate Representation Execution Environment), a powerful and flexible runtime designed to accelerate and simplify ML deployment across various platforms. In this blog post, we will explore the critical features of IREE and discover how it is revolutionizing how developers deploy ML models.

  1. Cross-Platform Deployment: IREE enables ML developers to deploy their models on various platforms, including mobile, desktop, and embedded systems. Its platform-agnostic approach allows developers to write their code once and run it seamlessly across different hardware configurations, eliminating the need for platform-specific code.
  2. Integration with MLIR: IREE leverages the power of MLIR, a flexible and extensible compiler infrastructure tailored for machine learning workloads. MLIR’s multi-level intermediate representation and optimizations ensure that IREE can efficiently handle complex ML tasks, resulting in improved performance and reduced memory footprint.
  3. Support for Multiple ML Frameworks: IREE is designed to work seamlessly with popular ML frameworks, such as TensorFlow and PyTorch. This compatibility allows developers to integrate IREE into their existing workflows with minimal effort, further streamlining the deployment process.
  4. Hardware-Agnostic Optimizations: IREE’s focus on hardware-agnostic optimizations ensures that it can efficiently execute ML workloads on a wide range of hardware platforms, including CPUs, GPUs, and dedicated ML accelerators. This flexibility empowers developers to harness the full potential of their AI solutions without being locked into a specific hardware ecosystem.
  5. Open-Source and Community-Driven: As an open-source project, IREE encourages collaboration and contribution from developers worldwide. This collective effort ensures that IREE continues to grow and adapt to the ever-evolving landscape of AI and machine learning, further enhancing its capabilities and offering a more flexible and efficient solution for ML deployment.

IREE is the game-changing runtime that can revolutionize your machine-learning deployment process. It enables cross-platform deployment, MLIR integration, compatibility with popular ML frameworks, and hardware-agnostic optimizations, providing developers an invaluable tool to optimize and streamline their ML workloads.

Unlocking Cross-Platform Compatibility with StableHLO: A Bridge Between ML Frameworks and Compilers

In the rapidly evolving landscape of AI and machine learning, developers often need help to ensure compatibility between various ML frameworks and compilers. Enter StableHLO, a robust portability layer that bridges the gap between ML frameworks and ML compilers, streamlining the development process and enhancing cross-platform compatibility.

StableHLO is an operation set designed explicitly for high-level operations (HLO) that boasts dynamism, quantization, and sparsity support. Its unique capabilities enable it to connect robustly between major ML frameworks, such as JAX, PyTorch, TensorFlow, and ML compilers.

One of the standout features of StableHLO is its ability to be serialized into MLIR bytecode. This serialization ensures compatibility guarantees across various platforms, making it an invaluable tool for developers seeking a seamless and efficient workflow when working with different ML frameworks and compilers.

Throughout 2023, the team behind StableHLO is committed to working closely with the PyTorch team to enable seamless integration with the recent PyTorch 2.0 release. This collaboration will further enhance the capabilities of StableHLO and solidify its position as a critical component in the AI and machine learning development ecosystem.

StableHLO is an innovative solution that simplifies cross-platform compatibility in ML development, connecting ML frameworks and compilers through a powerful and flexible operation set. By incorporating StableHLO into your workflow, you can ensure smoother integration and harness the full potential of various ML frameworks and compilers.

Understanding Custom Calls with XLA: A Walkthrough of a TensorFlow Code Example

Let us walk through one of the code examples from TensorFlow’s XLA documentation to help you better understand how custom calls work within the XLA compiler. Custom calls provide a way to extend the XLA compiler’s capabilities by allowing you to define your custom operations, which can be integrated into an XLA computation.

import numpy as np
import tensorflow as tf
from tensorflow.compiler.xla.experimental.xla_sharding import xla_sharding

def topk_custom_call(x, k):
xla_op = tf.raw_ops.XlaHostCompute(
Tinputs=[x],
Tout=[tf.float32, tf.int32],
key="TopK",
shape=[(k,), (k,)],
dynamic_slice_start=[0],
dynamic_slice_sizes=[k],
device_ordinal=0,
func=lambda x: (tf.math.top_k(x, k=k).values, tf.math.top_k(x, k=k).indices))
return xla_op

k = 3
x = tf.Variable(np.random.randn(10), dtype=tf.float32)
with tf.device("device:XLA_CPU:0"):
topk_result = topk_custom_call(x, k)
xla_sharding.unshard(topk_result)

We’re implementing a custom call for the Top-K operation using the XLA compiler in this example. The Top-K operation returns the top K elements and their corresponding indices from an input tensor x.

  1. First, we import the necessary libraries, including TensorFlow, NumPy, and xla_sharding, from the XLA compiler.
  2. We define a function topk_custom_call(x, k) that takes an input tensor x and an integer k as arguments. Inside this function, we utilize the tf.raw_ops.XlaHostCompute function to create our custom call. This function requires a set of parameters, such as input tensors, output data types, a key, output shapes, and a function for performing the Top-K operation. The func parameter defines the process using TensorFlow's built-in tf.math.top_k() function to compute the top K values and their indices.
  3. We set the value of k to 3, and create a TensorFlow variable x with 10 random elements.
  4. Using the with tf.device("device:XLA_CPU:0") context manager, we specify that the computation should be performed on the XLA_CPU device. Inside this context, we call our custom topk_custom_call(x, k) function, which returns the top K elements and their indices in the input tensor x.
  5. Finally, we use xla_sharding.unshard(topk_result) to convert the sharded result back into a regular TensorFlow tensor.

With this code example, you can see how custom calls in XLA enable you to define your operations and seamlessly integrate them into an XLA computation, making it a powerful tool for extending the capabilities of the XLA compiler.

Let’s show how to create a custom call on the CPU

We are using TensorFlow for C++ here:

#include "tensorflow/compiler/xla/client/xla_builder.h"
#include "tensorflow/compiler/xla/service/custom_call_target_registry.h"

void do_it() {
xla::XlaBuilder b("do_it");
xla::XlaOp param0 =
xla::Parameter(&b, 0, xla::ShapeUtil::MakeShape(xla::F32, {128}), "p0");
xla::XlaOp param1 =
xla::Parameter(&b, 1, xla::ShapeUtil::MakeShape(xla::F32, {2048}), "p1");
xla::XlaOp custom_call =
xla::CustomCall(&b, "do_custom_call", /*operands=*/{param0, param1},
/*shape=*/xla::ShapeUtil::MakeShape(xla::F32, {2048}));
}

void do_custom_call(void* out, const void** in) {
float* out_buf = reinterpret_cast<float*>(out);
const float* in0 = reinterpret_cast<const float*>(in[0]);
const float* in1 = reinterpret_cast<const float*>(in[1]);
for (int i = 0; i < 2048; ++i) {
out_buf[i] = in0[i % 128] + in1[i];
}
}
XLA_REGISTER_CUSTOM_CALL_TARGET(do_custom_call, "Host");

The function do_custom_call requires the sizes of the buffers it operates on. This example hardcodes these as 128 and 2048. Alternatively, you can pass the sizes in as parameters to the call.

Now do this on GPU

void do_it() { /* same implementation as above */ }

__global__ custom_call_kernel(const float* in0, const float* in1, float* out) {
size_t idx = blockIdx.x * blockDim.x + threadIdx.x;
out[idx] = in0[idx % 128] + in1[idx];
}

void do_custom_call(CUstream stream, void** buffers,
const char* opaque, size_t opaque_len) {
const float* in0 = reinterpret_cast<const float*>(buffers[0]);
const float* in1 = reinterpret_cast<const float*>(buffers[1]);
float* out = reinterpret_cast<float*>(buffers[2]);

const int64_t block_dim = 64;
const int64_t grid_dim = 2048 / block_dim;
custom_call_kernel<<<grid_dim, block_dim,
/*dynamic_shared_mem_bytes=*/0, stream>>>(in0, in1, out);
}
XLA_REGISTER_CUSTOM_CALL_TARGET(do_custom_call, "CUDA");

It launches the CUDA kernel, but you could easily change it to another AI accelerator. See the full example here.

A Step-by-Step Guide to Using XLA with PyTorch

In this part, we’ll walk you through a step-by-step example of using XLA with PyTorch to accelerate your machine learning workloads. XLA, short for Accelerated Linear Algebra, is a domain-specific compiler designed to optimize TensorFlow computations. However, with the PyTorch-XLA project, we can now use XLA to boost the performance of PyTorch models.

Let’s get started with a simple example of training a neural network on the MNIST dataset using PyTorch and XLA.

Step 1: Install the PyTorch-XLA package

To use XLA with PyTorch, you’ll need to install the torch-xla package. Follow the instructions for your specific platform from the PyTorch-XLA GitHub repository.

Step 2: Import required libraries

First, import the necessary libraries:

import torch
import torch.nn as nn
import torch.optim as optim
import torch_xla
import torch_xla.core.xla_model as xm
import torch_xla.distributed.xla_multiprocessing as xmp
from torchvision import datasets, transforms

Step 3: Define the neural network model

Next, let’s define a simple neural network model:

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = self.dropout1(x)
x = torch.flatten(x, 1)
x = self.fc1(x)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x)
output = F.log_softmax(x, dim=1)
return output

Step 4: Define the training and testing loops

Now, let’s define the training and testing loops:

def train(model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 10 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))

def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum')
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])

train_dataset = datasets.MNIST('../data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('../data', train=False, transform=transform)

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1000, shuffle=False)

Step 6: Initialize the model, optimizer, and device

Now, let’s initialize the model, optimizer, and device. We will use the XLA device for training:

device = xm.xla_device()
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

Step 7: Train and test the model

Finally, let’s train and test the model:

num_epochs = 10

for epoch in range(1, num_epochs + 1):
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)

That’s it! Following these steps, you’ve successfully trained and tested a neural network on the MNIST dataset using PyTorch and XLA. This example demonstrates how easy it is to integrate XLA into your PyTorch workflows to accelerate and optimize your machine learning workloads.

Conclusion

This is what I have learned so far regarding OpenXLA and how it is a game-changer in the domain of AI frameworks. The lock-in has been a blocker when choosing one ecosystem over another and later wanting to update the hardware to a new generation or brand. The next step will be deep diving into writing code and integrating other frameworks and devices into OpenXLA. I will investigate how to utilize OpenXLA when training and inference on 4th generation Intel® Xeon® Scalable Processors, Intel® Data Center GPU Flex Series, and other AI accelerators. Now moving from one architecture to another will be easy, and building a heterogenic solution will, from now on, be standard practice because of the different strengths of each accelerator.

To be continued in my SubStack

--

--

Björn Runåker
Björn Runåker

Written by Björn Runåker

Software developer into deep learning in combination of Big Data and security

No responses yet