Optimizing PyTorch Mannequin Inference on AWS Graviton

0
1102
Optimizing PyTorch Mannequin Inference on AWS Graviton


AI/ML fashions might be an especially costly endeavor. Lots of our posts have been centered on all kinds of suggestions, tips, and methods for analyzing and optimizing the runtime efficiency of AI/ML workloads. Our argument has been twofold:

  1. Efficiency evaluation and optimizations should be an integral course of of each AI/ML growth venture, and,
  2. Reaching significant efficiency boosts and value discount doesn’t require a excessive diploma of specialization. Any AI/ML developer can do it. Each AI/ML developer ought to do it.

, we addressed the problem of optimizing an ML inference workload on an Intel® Xeon® processor. We started by reviewing quite a lot of situations by which a CPU is likely to be your best option for AI/ML inference even in an period of a number of devoted AI inference chips. We then launched a toy image-classification PyTorch mannequin and proceeded to reveal a large variety of methods for reinforcing its runtime efficiency on an Amazon EC2 c7i.xlarge occasion, powered by 4th Technology Intel Xeon Scalable processors. On this publish, we prolong our dialogue to AWS’s homegrown Arm-based Graviton CPUs. We are going to revisit most of the optimizations we mentioned in our earlier posts — a few of which would require adaptation to the Arm processor — and assess their impression on the identical toy mannequin. Given the profound variations between the Arm and Intel processors, the paths to the perfect performing configuration might take completely different paths.

AWS Graviton

AWS Graviton is a household of processors primarily based on Arm Neoverse CPUs, which are customized and constructed by AWS for optimum price-performance and power effectivity. Their devoted engines for vector processing (NEON and SVE/SVE2) and matrix multiplication (MMLA), and their help for Bfloat16 operations (as of Graviton3), make them a compelling candidate for working compute intensive workloads equivalent to AI/ML inference. To facilitate high-performance AI/ML on Graviton, all the software program stack has been optimized for its use:

  • Low-Degree Compute Kernels from the Arm Compute Library (ACL) are extremely optimized to leverage the Graviton {hardware} accelerators (e.g., SVE and MMLA).
  • ML Middleware Libraries equivalent to oneDNN and OpenBLAS route deep studying and linear algebra operations to the specialised ACL kernels.
  • AI/ML Frameworks like PyTorch and TensorFlow are compiled and configured to make use of these optimized backends.

On this publish we are going to use an Amazon EC2 c8g.xlarge occasion powered by 4 AWS Graviton4 processors and an AWS ARM64 PyTorch Deep Studying AMI (DLAMI).

The intention of this publish is to reveal suggestions for reinforcing efficiency on an AWS Graviton occasion. Importantly, our intention is not to attract a comparability between AWS Graviton and different chips, neither is it to advocate for using one chip over the opposite. The only option of processor will depend on an entire bunch of concerns past the scope of this publish. One of many vital concerns would be the most runtime efficiency of your mannequin on every chip. In different phrases: how a lot “bang” can we get for our buck? Thus, making an knowledgeable determination about the perfect processor is among the motivations for optimizing runtime efficiency on each.

One other motivation for optimizing our mannequin’s efficiency for a number of inference units, is to extend its portability. The enjoying discipline of AI/ML is extraordinarily dynamic and resilience to altering circumstances is essential for achievement. It’s not unusual for compute cases of sure varieties to out of the blue develop into unavailable or scarce. Conversely, a rise in capability of AWS Graviton cases, may suggest their availability at steep reductions, e.g., within the Amazon EC2 Spot Occasion market, presenting cost-savings alternatives that you wouldn’t need to miss out on.

Disclaimers

The blocks code of code we are going to share, the optimization steps we are going to focus on, and the outcomes we are going to attain, are supposed for example of the advantages you may even see from ML efficiency optimization on an AWS Graviton occasion. These might differ significantly from the outcomes you may see with your personal mannequin and runtime setting. Please don’t depend on the accuracy or optimality of the contents of this publish. Please don’t interpret the point out of any library, framework, or platform as an endorsement of its use.

Inference Optimization on AWS Graviton

As in our earlier publish, we are going to reveal the optimization steps on a toy picture classification mannequin:

import torch, torchvision
import time


def get_model(channels_last=False, compile=False):
    mannequin = torchvision.fashions.resnet50()

    if channels_last:
        mannequin= mannequin.to(memory_format=torch.channels_last)

    mannequin = mannequin.eval()

    if compile:
        mannequin = torch.compile(mannequin)

    return mannequin

def get_input(batch_size, channels_last=False):
    batch = torch.randn(batch_size, 3, 224, 224)
    if channels_last:
        batch = batch.to(memory_format=torch.channels_last)
    return batch

def get_inference_fn(mannequin, enable_amp=False):
    def infer_fn(batch):
        with torch.inference_mode(), torch.amp.autocast(
                'cpu',
                dtype=torch.bfloat16,
                enabled=enable_amp
        ):
            output = mannequin(batch)
        return output
    return infer_fn

def benchmark(infer_fn, batch):
    # warm-up
    for _ in vary(20):
        _ = infer_fn(batch)

    iters = 100

    begin = time.time()
    for _ in vary(iters):
        _ = infer_fn(batch)
    finish = time.time()

    return (finish - begin) / iters


batch_size = 1
mannequin = get_model()
batch = get_input(batch_size)
infer_fn = get_inference_fn(mannequin)
avg_time = benchmark(infer_fn, batch)
print(f"nAverage samples per second: {(batch_size/avg_time):.2f}")

The preliminary throughput is 12 samples per second (SPS).

Improve to the Most Latest PyTorch Launch

Whereas the model of PyTorch in our DLAMI is 2.8, the most recent model of PyTorch, on the time of this writing, is 2.9. Given the speedy tempo of growth within the discipline of AI/ML, it’s extremely really helpful to make use of essentially the most up-to-date library packages. As our first step, we improve to PyTorch 2.9 which consists of key updates to its Arm backend.

pip3 set up -U torch torchvision --index-url https://obtain.pytorch.org/whl/cpu

Within the case of our mannequin in its preliminary configuration, upgrading the PyTorch model doesn’t have any impact. Nevertheless, this step is essential for getting essentially the most out of the optimization methods that we are going to assess.

Batched Inference

To cut back the overhead of launching overheads and enhance the utilization of the HW accelerators, we group collectively samples and apply batched inference. The desk under demonstrates how the mannequin throughput varies as a operate of batch measurement:

Inference Throughput for Various Batch Sizes (by Writer)

Reminiscence Optimizations

We apply quite a lot of methods from our earlier publish for optimizing reminiscence allocation and utilization. These embody the channels-last reminiscence format, automated blended precision with the bfloat16 knowledge sort (supported from Graviton3), the TCMalloc allocation library, and large web page allocation. Please see the  for particulars. We additionally allow the quick math mode of the ACL GEMM kernels, and caching of the kernel primitives — two optimizations that seem within the official pointers for working PyTorch inference on Graviton.

The command line directions required to allow these optimizations are proven under:

# set up TCMalloc
sudo apt-get set up google-perftools

# Program using TCMalloc
export LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libtcmalloc.so.4

# Allow enormous web page reminiscence allocation
export THP_MEM_ALLOC_ENABLE=1

# Allow the quick math mode of the GEMM kernels
export DNNL_DEFAULT_FPMATH_MODE=BF16

# Set LRU Cache capability to cache the kernel primitives
export LRU_CACHE_CAPACITY=1024

The next desk captures the impression of the reminiscence optimizations, utilized successively:

ResNet-50 Reminiscence Optimization Outcomes (by Writer)

Within the case of our toy mannequin, the channels-last and bfloat16-mixed precision optimizations had the best impression. After making use of all the reminiscence optimizations, the typical throughput is 53.03 SPS.

Mannequin Compilation

The help of PyTorch compilation for AWS Graviton is an space of centered effort of the AWS Graviton staff. Nevertheless, within the case of our toy mannequin, it leads to a slight discount in throughput, from 53.03 SPS to 52.23.

Multi-Employee Inference

Whereas sometimes utilized in settings with many greater than 4 vCPUs, we reveal the implementation of multi-worker inference by modifying our script to help core pinning:

if __name__ == '__main__':
    # pin CPUs in accordance with employee rank
    import os, psutil
    rank = int(os.environ.get('RANK','0'))
    world_size = int(os.environ.get('WORLD_SIZE','1'))
    cores = record(vary(psutil.cpu_count(logical=True)))
    num_cores = len(cores)
    cores_per_process = num_cores // world_size
    start_index = rank * cores_per_process
    end_index = (rank + 1) * cores_per_process
    pid = os.getpid()
    p = psutil.Course of(pid)
    p.cpu_affinity(cores[start_index:end_index])

    batch_size = 8
    mannequin = get_model(channels_last=True)
    batch = get_input(batch_size, channels_last=True)
    infer_fn = get_inference_fn(mannequin, enable_amp=True)
    avg_time = benchmark(infer_fn, batch)
    print(f"nAverage samples per second: {(batch_size/avg_time):.2f}")

We observe that opposite to different AWS EC2 CPU occasion varieties, every Graviton vCPU maps on to a single bodily CPU core. We use the torchrun utility to start out up 4 staff, with every working on a single CPU core:

export OMP_NUM_THREADS=1 #set one OpenMP thread per employee
torchrun --nproc_per_node=4 important.py

This leads to a throughput of 55.15 SPS, a 4% enchancment over our earlier greatest consequence.

INT8 Quantization for Arm

One other space of lively growth and steady enchancment on Arm is INT8 quantization. INT8 quantization instruments are sometimes closely tied to the goal occasion sort. In our earlier publish we demonstrated PyTorch 2 Export Quantization with X86 Backend by means of Inductor utilizing the TorchAO (0.12.1) library. Thankfully, latest variations of TorchAO embody a devoted quantizer for Arm. The up to date quantization sequence is proven under. As in our earlier publish we have an interest simply within the potential efficiency impression. In apply, INT8 quantization can have a big impression on the standard of the mannequin and will necessitate a extra subtle quantization technique.

from torchao.quantization.pt2e.quantize_pt2e import prepare_pt2e, convert_pt2e
import torchao.quantization.pt2e.quantizer.arm_inductor_quantizer as aiq

def quantize_model(mannequin):
    x = torch.randn(4, 3, 224, 224).contiguous(
                            memory_format=torch.channels_last)
    example_inputs = (x,)
    batch_dim = torch.export.Dim("batch")
    with torch.no_grad():
        exported_model = torch.export.export(
            mannequin,
            example_inputs,
            dynamic_shapes=((batch_dim,
                             torch.export.Dim.STATIC,
                             torch.export.Dim.STATIC,
                             torch.export.Dim.STATIC),
                            )
        ).module()
    quantizer = aiq.ArmInductorQuantizer()
    quantizer.set_global(aiq.get_default_arm_inductor_quantization_config())
    prepared_model = prepare_pt2e(exported_model, quantizer)
    prepared_model(*example_inputs)
    converted_model = convert_pt2e(prepared_model)
    optimized_model = torch.compile(converted_model)
    return optimized_model


batch_size = 8
mannequin = get_model(channels_last=True)
mannequin = quantize_model(mannequin)
batch = get_input(batch_size, channels_last=True)
infer_fn = get_inference_fn(mannequin, enable_amp=True)
avg_time = benchmark(infer_fn, batch)
print(f"nAverage samples per second: {(batch_size/avg_time):.2f}")

The resultant throughput is 56.77 SPS for a 7.1% enchancment over the bfloat16 resolution.

AOT Compilation Utilizing ONNX and OpenVINO

In our earlier publish, we explored ahead-of-time (AOT) mannequin compilation methods utilizing Open Neural Community Trade (ONNX) and OpenVINO. Each libraries embody devoted help for working on AWS Graviton (e.g., see right here and right here). The experiments on this part require the next library installations:

pip set up onnxruntime onnxscript openvino nncf

The next code block demonstrates the mannequin compilation and execution on Arm utilizing ONNX:

def export_to_onnx(mannequin, onnx_path="resnet50.onnx"):
    dummy_input = torch.randn(4, 3, 224, 224)
    batch = torch.export.Dim("batch")
    torch.onnx.export(
        mannequin,
        dummy_input,
        onnx_path,
        input_names=["input"],
        output_names=["output"],
        dynamic_shapes=((batch,
                         torch.export.Dim.STATIC,
                         torch.export.Dim.STATIC,
                         torch.export.Dim.STATIC),
                        ),
        dynamo=True
    )
    return onnx_path

def onnx_infer_fn(onnx_path):
    import onnxruntime as ort

    sess = ort.InferenceSession(
        onnx_path,
        suppliers=["CPUExecutionProvider"]
   )
    sess_options = ort.SessionOptions()
    sess_options.add_session_config_entry(
               "mlas.enable_gemm_fastmath_arm64_bfloat16", "1")
    input_name = sess.get_inputs()[0].identify

    def infer_fn(batch):
        consequence = sess.run(None, {input_name: batch})
        return consequence
    return infer_fn

batch_size = 8
mannequin = get_model()
onnx_path = export_to_onnx(mannequin)
batch = get_input(batch_size).numpy()
infer_fn = onnx_infer_fn(onnx_path)
avg_time = benchmark(infer_fn, batch)
print(f"nAverage samples per second: {(batch_size/avg_time):.2f}")

It ought to be famous that the ONNX runtime helps a devoted ACL-ExecutionProvider for working on Arm, however this requires a customized ONNX construct (as of the time of this writing), which is out of the scope of this publish.

Alternatively, we are able to compile the mannequin utilizing OpenVINO. The code block under demonstrates its use, together with an choice for INT8 quantization utilizing NNCF:

import openvino as ov
import nncf

def openvino_infer_fn(compiled_model):
    def infer_fn(batch):
        consequence = compiled_model([batch])[0]
        return consequence
    return infer_fn

class RandomDataset(torch.utils.knowledge.Dataset):
    def __len__(self):
        return 10000

    def __getitem__(self, idx):
        return torch.randn(3, 224, 224)

quantize_model = False
batch_size = 8
mannequin = get_model()
calibration_loader = torch.utils.knowledge.DataLoader(RandomDataset())
calibration_dataset = nncf.Dataset(calibration_loader)

if quantize_model:
    # quantize PyTorch mannequin
    mannequin = nncf.quantize(mannequin, calibration_dataset)

ovm = ov.convert_model(mannequin, example_input=torch.randn(1, 3, 224, 224))
ovm = ov.compile_model(ovm)
batch = get_input(batch_size).numpy()
infer_fn = openvino_infer_fn(ovm)
avg_time = benchmark(infer_fn, batch)
print(f"nAverage samples per second: {(batch_size/avg_time):.2f}")

Within the case of our toy mannequin, OpenVINO compilation leads to a further enhance of the throughput to 63.48 SPS, however the NNCF quantization disappoints, leading to simply 55.18 SPS.

Outcomes

The outcomes of our experiments are summarized within the desk under:

ResNet50 Inference Optimization Outcomes (by Writer)

As in our , we reran our experiments on a second mannequin — a Imaginative and prescient Transformer (ViT) from the timm library — to reveal how the impression of the runtime optimizations we mentioned can range primarily based on the small print of the mannequin. The outcomes are captured under:

ViT Inference Optimization Outcomes (by Writer)

Abstract

On this publish, we reviewed quite a lot of comparatively easy optimization methods and utilized them to 2 toy PyTorch fashions. Because the outcomes demonstrated, the impression of every optimization step can range tremendously primarily based on the small print of the mannequin, and the journey towards peak efficiency can take many various paths. The steps we introduced on this publish have been simply an appetizer; there are undoubtedly many extra optimizations that may unlock even higher efficiency.

Alongside the best way, we famous the numerous AI/ML libraries which have launched deep help for the Graviton structure, and the seemingly steady group effort of ongoing optimization. The efficiency positive aspects we achieved, mixed with this obvious dedication, show that AWS Graviton is firmly within the “huge leagues” in terms of working compute-intensive AI/ML workloads.