Use Lexer for Model Benchmarking

In this tutorial, we demonstrate how to use the Lexer library with a PyTorch Super-resolution model used in PyTorch's tutorial's for running models using ONNXRuntime.

Introduction to ONNX and ONNXRuntime

Here we'll provide a quick summary of the Open Neural Network Exchange (ONNX).

ONNX, as described by the organization, is:

an open format built to represent machine learning models. ONNX defines a common set of operators - the building blocks of machine learning and deep learning models - and a common file format to enable AI developers to use models with a variety of frameworks, tools, runtimes, and compilers.

The sibling project ONNX Runtime "automatically parses through your model to identify optimization opportunities and provides access to the best hardware acceleration available."

If you'd like to learn more, we recommend:

Comparing Your Model's Results Between PyTorch and ONNXRuntime

You'll want to start your script with imports for:

  • PyTorch
  • ONNX to access the ONNX Runtime library
  • The Lexer Context and Decorator
# Third party imports
import torch.onnx
import torch.utils.model_zoo as model_zoo
from torch import nn
from torch.nn import init

# Lexer imports
from lexer.api.lexer_context import LexerContext
from lexer.decorators.lexer import Lexer

Apply the Lexer Decorator to a Model Class

If you've read our guide on the Lexer Decorator, you'll be ready to apply @Lexer decorator to a class. The example class below is a model example from PyTorch).

@Lexer(
    input_names=["input"],
    output_names=["output"],
)
class SuperResolutionNet(nn.Module):
    def __init__(self, upscale_factor, inplace=False):
        super(SuperResolutionNet, self).__init__()

        self.relu = nn.ReLU(inplace=inplace)
        self.conv1 = nn.Conv2d(1, 64, (5, 5), (1, 1), (2, 2))
        self.conv2 = nn.Conv2d(64, 64, (3, 3), (1, 1), (1, 1))
        self.conv3 = nn.Conv2d(64, 32, (3, 3), (1, 1), (1, 1))
        self.conv4 = nn.Conv2d(32, upscale_factor**2, (3, 3), (1, 1), (1, 1))
        self.pixel_shuffle = nn.PixelShuffle(upscale_factor)

        self._initialize_weights()

    def forward(self, x):
        x = self.relu(self.conv1(x))
        x = self.relu(self.conv2(x))
        x = self.relu(self.conv3(x))
        x = self.pixel_shuffle(self.conv4(x))
        return x

    def _initialize_weights(self):
        init.orthogonal_(self.conv1.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv2.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv3.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv4.weight)

Initiate Your Model and Benchmark With Lexer

With the class defined and the Lexer decorator applied, you can now prep your model for execution, call the Lexer benchmark() API, and print out the benchmarking results.

if __name__ == "__main__":

    # Create the super-resolution model by using the above model definition.
    torch_model = SuperResolutionNet(upscale_factor=3)

    # Load pretrained model weights
    model_url = "https://s3.amazonaws.com/pytorch/test_data/export/superres_epoch100-44c6958e.pth"
    batch_size = 1  # just a random number

    # Initialize model with the pretrained weights
    map_location = lambda storage, loc: storage
    if torch.cuda.is_available():
        map_location = None
    torch_model.load_state_dict(
        model_zoo.load_url(model_url, map_location=map_location)
    )

    # set the model to inference mode
    torch_model.eval()

    # Input to the model
    input_tensor = torch.randn(batch_size, 1, 224, 224, requires_grad=True)

    # ------------------ Model Prep Ends, Lexer Benchmarking Begins ----------------------------------------------

    lexer_context = LexerContext()

    lexer_context.benchmark(
        torch_model=torch_model,
        input=input_tensor,
        num_iterations=5,
        batch_size=batch_size,
    )
    lexer_context.flush(name="benchmark", target_directory=".")
  
    print(torch_model.lexer.schema_json(indent=2))

The Full SuperResolutionNet with Lexer Script

Piecing it all together gives you the below one file script that you can save to a .py file and execute.

# Third party imports
import torch.onnx
import torch.utils.model_zoo as model_zoo
from torch import nn
from torch.nn import init

# Lexer imports
from lexer.api.lexer_context import LexerContext
from lexer.decorators.lexer import Lexer


@Lexer(
    input_names=["input"],
    output_names=["output"],
)
class SuperResolutionNet(nn.Module):
    def __init__(self, upscale_factor, inplace=False):
        super(SuperResolutionNet, self).__init__()

        self.relu = nn.ReLU(inplace=inplace)
        self.conv1 = nn.Conv2d(1, 64, (5, 5), (1, 1), (2, 2))
        self.conv2 = nn.Conv2d(64, 64, (3, 3), (1, 1), (1, 1))
        self.conv3 = nn.Conv2d(64, 32, (3, 3), (1, 1), (1, 1))
        self.conv4 = nn.Conv2d(32, upscale_factor**2, (3, 3), (1, 1), (1, 1))
        self.pixel_shuffle = nn.PixelShuffle(upscale_factor)

        self._initialize_weights()

    def forward(self, x):
        x = self.relu(self.conv1(x))
        x = self.relu(self.conv2(x))
        x = self.relu(self.conv3(x))
        x = self.pixel_shuffle(self.conv4(x))
        return x

    def _initialize_weights(self):
        init.orthogonal_(self.conv1.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv2.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv3.weight, init.calculate_gain("relu"))
        init.orthogonal_(self.conv4.weight)


if __name__ == "__main__":

    # Create the super-resolution model by using the above model definition.
    torch_model = SuperResolutionNet(upscale_factor=3)

    # Load pretrained model weights
    model_url = "https://s3.amazonaws.com/pytorch/test_data/export/superres_epoch100-44c6958e.pth"
    batch_size = 1  # just a random number

    # Initialize model with the pretrained weights
    map_location = lambda storage, loc: storage
    if torch.cuda.is_available():
        map_location = None
    torch_model.load_state_dict(
        model_zoo.load_url(model_url, map_location=map_location)
    )

    # set the model to inference mode
    torch_model.eval()

    # Input to the model
    input_tensor = torch.randn(batch_size, 1, 224, 224, requires_grad=True)

    # ------------------ Model Prep Ends, Lexer Benchmarking Begins ----------------------------------------------

    lexer_context = LexerContext()

    lexer_context.benchmark(
        torch_model=torch_model,
        input=input_tensor,
        num_iterations=5,
        batch_size=batch_size,
    )
    lexer_context.flush(name="benchmark", target_directory=".")

    print(torch_model.lexer.schema_json(indent=2))

See Benchmark Results

By executing the script from your terminal, you should now see results like the below, which includes JSON of your benchmark metrics.

You'll notice that you'll get two objects returned in the results array.

The default Lexer Configuration enables two engines to be benchmarked: Torch and ONNXRuntime.

The key insight for you as a user will be able to compare how the same model performs when executed through two different engines, and how those engines impact performance!

Results generated and exported to SuperResolutionNet.csv!
Generated results:
-------------------
[{'num_samples': 5, 'variance': 6.403878733313712e-06, 'mean_ms': 27.737423396320082, 'p50_ms': 26.324664999265224, 'p90_ms': 30.89536899351515, 'p95_ms': 31.47643099364359, 'p99_ms': 31.941280593746338, 'throughput_per_sec': 36.0523753670887, 'engine': 'onnxruntime', 'version': '1.13.1', 'batch_size': 1, 'optimizer': 'N/A', 'providers': 'N/A', 'device': 'cpu', 'timestamp': '2023-01-24 01:48:15.215648'}, 
 {'num_samples': 5, 'variance': 5.308219160244051e-06, 'mean_ms': 36.46238420042209, 'p50_ms': 35.657344997162, 'p90_ms': 39.08862060052343, 'p95_ms': 39.98508080258034, 'p99_ms': 40.70224896422587, 'throughput_per_sec': 27.425524192365454, 'engine': 'torch', 'version': '1.13.1', 'batch_size': 1, 'optimizer': 'N/A', 'providers': 'N/A', 'device': 'cpu', 'timestamp': '2023-01-24 01:48:15.215648'}]

By including the flush function, you will also get same data in CSV format as part of the Lexer artifact folder.