NOTE: The Jupyter Notebook below is included in the Chimera SDK and can be run interactively by running the following CLI command:
$ quadric sdk notebook
From the Jupyter Notebook window in your browser, select the notebook named /quadric/sdk-cli/examples/models/swin/QAT-Swin.ipynb.
PyTorch QAT -> ONNX Runtime Pipeline (Swin-B)
In this tutorial we use the PyTorch 2 Export (pt2e) library to perform quantization-aware training (QAT) on Swin-B, and export it such that it can be run through ONNX Runtime.
Notes:
- Training is expected to be done with a GPU, which can be mounted to the docker container with the option
--gpus allor--gpus device=0(replace 0 with whichever device you'd like to use)- Original training results were produced with an NVIDIA GeForce RTX 3090
pt2eis still in prototype phase (as of 12/19/2024), breaking changes may occur later- Other PyTorch quantization libraries are available, but we currently make no guarantee the process will work as intended using them
- Each network requires slightly different post-processing, so not all networks may be supported yet with the post-processing steps currently implemented
- Images, labels, and models used in this notebook conform to the ImageNet-1K standard (i.e. use 224x224 image resolution) and have subjects represented in the 1000 ImageNet classes; any different input size or class labels will require user-supplied datasets
High Level Overview
- Set up training helper functions
- Define our quantization configuration
- Prepare the training dataset
- Load the model
- Perform QAT using the
pt2elibrary - ONNX export and post-processing
- Prepare the validation dataset
- Validate and compare against FP32 model
0. Imports and setup
%pip install -r ../../requirements_gpu.txt
%pip install yacs timm termcolor # Used only for Swin
Requirement already satisfied: torch==2.5.0 in /usr/local/lib/python3.10/dist-packages (from -r ../../requirements_gpu.txt (line 1)) (2.5.0)
Requirement already satisfied: torchvision==0.20.0 in /usr/local/lib/python3.10/dist-packages (from -r ../../requirements_gpu.txt (line 2)) (0.20.0)
Requirement already satisfied: s3fs in /usr/local/lib/python3.10/dist-packages (from -r ../../requirements_gpu.txt (line 3)) (2026.6.0)
Requirement already satisfied: onnxscript==0.2.0 in /usr/local/lib/python3.10/dist-packages (from -r ../../requirements_gpu.txt (line 4)) (0.2.0)
Requirement already satisfied: nvidia-curand-cu12==10.3.5.147 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (10.3.5.147)
Requirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (1.13.1)
Requirement already satisfied: nvidia-cuda-runtime-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.127)
Requirement already satisfied: nvidia-cuda-cupti-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.127)
Requirement already satisfied: nvidia-cusolver-cu12==11.6.1.9 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (11.6.1.9)
Requirement already satisfied: typing-extensions>=4.8.0 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (4.15.0)
Requirement already satisfied: nvidia-cufft-cu12==11.2.1.3 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (11.2.1.3)
Requirement already satisfied: fsspec in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (2026.6.0)
Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (9.1.0.70)
Requirement already satisfied: triton==3.1.0 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (3.1.0)
Requirement already satisfied: networkx in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (2.8.5)
Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (3.16.1)
Requirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.127)
Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (3.1.6)
Requirement already satisfied: nvidia-nccl-cu12==2.21.5 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (2.21.5)
Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.127)
Requirement already satisfied: nvidia-cublas-cu12==12.4.5.8 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.5.8)
Requirement already satisfied: nvidia-nvjitlink-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.4.127)
Requirement already satisfied: nvidia-cusparse-cu12==12.3.1.170 in /usr/local/lib/python3.10/dist-packages (from torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (12.3.1.170)
Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from torchvision==0.20.0->-r ../../requirements_gpu.txt (line 2)) (1.24.4)
Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /usr/local/lib/python3.10/dist-packages (from torchvision==0.20.0->-r ../../requirements_gpu.txt (line 2)) (12.2.0)
Requirement already satisfied: packaging in /usr/local/lib/python3.10/dist-packages (from onnxscript==0.2.0->-r ../../requirements_gpu.txt (line 4)) (26.2)
Requirement already satisfied: ml_dtypes in /usr/local/lib/python3.10/dist-packages (from onnxscript==0.2.0->-r ../../requirements_gpu.txt (line 4)) (0.3.2)
Requirement already satisfied: onnx>=1.16 in /usr/local/lib/python3.10/dist-packages (from onnxscript==0.2.0->-r ../../requirements_gpu.txt (line 4)) (1.16.2)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.10/dist-packages (from sympy==1.13.1->torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (1.3.0)
Requirement already satisfied: aiobotocore<4.0.0,>=2.19.0 in /usr/local/lib/python3.10/dist-packages (from s3fs->-r ../../requirements_gpu.txt (line 3)) (3.7.0)
Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0 in /usr/local/lib/python3.10/dist-packages (from s3fs->-r ../../requirements_gpu.txt (line 3)) (3.14.1)
Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (0.13.0)
Requirement already satisfied: wrapt<3.0.0,>=1.10.10 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (2.2.1)
Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (2.9.0.post0)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.1.0)
Requirement already satisfied: botocore<1.43.1,>=1.42.90 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.43.0)
Requirement already satisfied: multidict<7.0.0,>=6.0.0 in /usr/local/lib/python3.10/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (6.7.1)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (2.6.2)
Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.8.0)
Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (26.1.0)
Requirement already satisfied: yarl<2.0,>=1.17.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.24.2)
Requirement already satisfied: propcache>=0.2.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (0.5.2)
Requirement already satisfied: aiosignal>=1.4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.4.0)
Requirement already satisfied: async-timeout<6.0,>=4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (5.0.1)
Requirement already satisfied: protobuf>=3.20.2 in /usr/local/lib/python3.10/dist-packages (from onnx>=1.16->onnxscript==0.2.0->-r ../../requirements_gpu.txt (line 4)) (4.25.3)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2->torch==2.5.0->-r ../../requirements_gpu.txt (line 1)) (3.0.3)
Requirement already satisfied: urllib3!=2.2.0,<3,>=1.25.4 in /usr/local/lib/python3.10/dist-packages (from botocore<1.43.1,>=1.42.90->aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.26.20)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil<3.0.0,>=2.1->aiobotocore<4.0.0,>=2.19.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (1.17.0)
Requirement already satisfied: idna>=2.0 in /usr/local/lib/python3.10/dist-packages (from yarl<2.0,>=1.17.0->aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs->-r ../../requirements_gpu.txt (line 3)) (3.18)
[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv[0m[33m
[0mNote: you may need to restart the kernel to use updated packages.
Requirement already satisfied: yacs in /usr/local/lib/python3.10/dist-packages (0.1.8)
Requirement already satisfied: timm in /usr/local/lib/python3.10/dist-packages (1.0.3)
Requirement already satisfied: termcolor in /usr/local/lib/python3.10/dist-packages (3.3.0)
Requirement already satisfied: PyYAML in /usr/local/lib/python3.10/dist-packages (from yacs) (6.0)
Requirement already satisfied: huggingface_hub in /usr/local/lib/python3.10/dist-packages (from timm) (0.36.2)
Requirement already satisfied: torchvision in /usr/local/lib/python3.10/dist-packages (from timm) (0.20.0)
Requirement already satisfied: safetensors in /usr/local/lib/python3.10/dist-packages (from timm) (0.8.0)
Requirement already satisfied: torch in /usr/local/lib/python3.10/dist-packages (from timm) (2.5.0)
Requirement already satisfied: hf-xet<2.0.0,>=1.1.3 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (1.5.1)
Requirement already satisfied: packaging>=20.9 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (26.2)
Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (2026.6.0)
Requirement already satisfied: tqdm>=4.42.1 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (4.66.4)
Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (4.15.0)
Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (3.16.1)
Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from huggingface_hub->timm) (2.31.0)
Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (3.1.6)
Requirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (1.13.1)
Requirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.127)
Requirement already satisfied: nvidia-cuda-runtime-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.127)
Requirement already satisfied: nvidia-cufft-cu12==11.2.1.3 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (11.2.1.3)
Requirement already satisfied: nvidia-cuda-cupti-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.127)
Requirement already satisfied: nvidia-cusolver-cu12==11.6.1.9 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (11.6.1.9)
Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.127)
Requirement already satisfied: nvidia-nccl-cu12==2.21.5 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (2.21.5)
Requirement already satisfied: nvidia-cublas-cu12==12.4.5.8 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.5.8)
Requirement already satisfied: networkx in /usr/local/lib/python3.10/dist-packages (from torch->timm) (2.8.5)
Requirement already satisfied: nvidia-nvjitlink-cu12==12.4.127 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.4.127)
Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (9.1.0.70)
Requirement already satisfied: nvidia-curand-cu12==10.3.5.147 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (10.3.5.147)
Requirement already satisfied: triton==3.1.0 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (3.1.0)
Requirement already satisfied: nvidia-cusparse-cu12==12.3.1.170 in /usr/local/lib/python3.10/dist-packages (from torch->timm) (12.3.1.170)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.10/dist-packages (from sympy==1.13.1->torch->timm) (1.3.0)
Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /usr/local/lib/python3.10/dist-packages (from torchvision->timm) (12.2.0)
Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from torchvision->timm) (1.24.4)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2->torch->timm) (3.0.3)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface_hub->timm) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface_hub->timm) (3.18)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface_hub->timm) (2026.6.17)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface_hub->timm) (1.26.20)
[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv[0m[33m
[0mNote: you may need to restart the kernel to use updated packages.
import os
import sys
import time
import copy
import logging
import itertools
import warnings
import tempfile
import random
import operator
from pathlib import Path
from dataclasses import dataclass
from datasets import load_from_disk
from typing import Optional
import numpy as np
from PIL import Image
import zipfile
import matplotlib.pyplot as plt
## Add project root to sys.path so `examples` can be imported as a package
_dir = Path(os.path.abspath(""))
while _dir != _dir.parent:
if (_dir / "setup.cfg").exists():
sys.path.insert(0, str(_dir))
break
_dir = _dir.parent
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.onnx
from torch.utils.data import DataLoader, Subset
from torch.export import export
from torch.ao.quantization.quantize_pt2e import (
prepare_qat_pt2e,
convert_pt2e,
)
from torch.ao.quantization.quantizer import (
Quantizer,
QuantizationSpec,
FixedQParamsQuantizationSpec,
QuantizationAnnotation,
)
from torch.ao.quantization.fake_quantize import FakeQuantize
from torch.ao.quantization.quantizer import QuantizationSpec
from torch.ao.quantization.observer import MovingAverageMinMaxObserver
from torch.fx.passes.utils.source_matcher_utils import get_source_partitions
import torchvision
from torchvision.datasets import ImageFolder
from torchvision.transforms.v2 import (
RandomResizedCrop,
RandomHorizontalFlip,
RandomVerticalFlip,
CenterCrop,
Compose,
Normalize,
Resize,
ToImage,
ToDtype,
)
from tvm.contrib.epu.chimera_job.chimera_job import ChimeraJob
from tvm.relay.backend.contrib.epu.util import logger as tvm_logger
from tvm.contrib.epu.chimera_job.constants import DEFAULT_ONNX_OPSET
import sdk_cli.lib.qat_processor as processor
from sdk_cli.lib.chimera_job import ChimeraJob
from sdk_cli.utils.dataloaders import CalibrationDataLoader
from sdk_cli.utils.datasets import ImageNet_Mini_Quadric, QuadricCalibration
from sdk_cli.utils.datasets.ImageNet import (
IMAGENET_1K_NORMALIZATION_PARAMETERS,
OPTIMIZED_IMAGENET_1K_LABELS,
)
from sdk_cli.utils.model_helpers import ClassifyResult
from sdk_cli.utils.performance_trackers import ClassifierPerformanceTracker
from sdk_cli.lib.quantize import (
QuantizationExperiment,
QuantizedONNXModel,
run_quantization_experiment,
compute_tensor_ranges,
)
from examples.models.zoo.zoo_utils import download_file, onnx_check_and_simplify
warnings.filterwarnings(action="ignore", category=DeprecationWarning, module=r".*")
warnings.filterwarnings(action="default", module=r"torch.ao.quantization")
## NOTE: below are manually set for reproducability
torch.manual_seed(191009)
np.random.seed(2147483648)
random.seed(2147483648)
torch.use_deterministic_algorithms(True)
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
torch.set_default_device("cuda")
1. Set up training helper functions
These will assist us in creating our training loop, based on those provided in the pt2e documentation page listed above.
class AverageMeter(object):
def __init__(self, name, fmt=":f"):
self.name = name
self.fmt = fmt
self.reset()
def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
def __str__(self):
return f"{name} {val:f} ({avg:f})"
def accuracy(output, target, topk=(1,)):
with torch.no_grad():
maxk = max(topk)
batch_size = target.size(0)
_, pred = output.topk(maxk, 1, True, True)
pred = pred.t()
correct = pred.eq(target.view(1, -1).expand_as(pred))
res = []
for k in topk:
correct_k = correct[:k].reshape(-1).float().sum(0, keepdim=True)
res.append(correct_k.mul_(100.0 / batch_size))
return res
def get_kd_loss(output_student, output_teacher, temp, scale):
kl_loss = nn.KLDivLoss(reduction="batchmean")
loss_kl = kl_loss(
F.log_softmax(output_student / temp, dim=1),
F.softmax(output_teacher / temp, dim=1),
)
return scale * loss_kl
def train_one_epoch_distill(
teacher,
student,
criterion,
optimizer,
temp,
kd_scale,
loss_data,
data_loader,
ntrain_batches,
):
top1 = AverageMeter("Acc@1")
top5 = AverageMeter("Acc@5")
avgloss = AverageMeter("Loss")
cnt = 0
for pair in data_loader:
start_time = time.time()
optimizer.zero_grad()
image, target = pair.values()
image = image.to("cuda")
target = target.to("cuda")
print(".", end="")
cnt += 1
student_output = student(image)
with torch.no_grad():
teacher_output = teacher(image)
ce_loss = criterion(student_output, target)
loss = ce_loss + get_kd_loss(student_output, teacher_output, temp, kd_scale)
optimizer.zero_grad()
loss.backward()
optimizer.step()
torch.cuda.synchronize()
acc1, acc5 = accuracy(student_output, target, topk=(1, 5))
top1.update(acc1[0], image.size(0))
top5.update(acc5[0], image.size(0))
avgloss.update(loss, image.size(0))
if cnt >= ntrain_batches:
print("Loss", avgloss.avg)
loss_data.append(avgloss.avg.item())
print(
"Training: * Acc@1 {top1.avg:.3f} Acc@5 {top5.avg:.3f}".format(top1=top1, top5=top5)
)
return
def evaluate(model, data_loader, neval_batches):
top1 = AverageMeter("Acc@1", ":6.2f")
top5 = AverageMeter("Acc@5", ":6.2f")
cnt = 0
with torch.no_grad():
for image, target in data_loader:
image = image.cuda(non_blocking=True)
target = target.cuda(non_blocking=True)
output = model(image)
cnt += 1
acc1, acc5 = accuracy(output, target, topk=(1, 5))
top1.update(acc1[0], image.size(0))
top5.update(acc5[0], image.size(0))
if cnt >= neval_batches:
return top1, top5
print("")
return top1, top5
def print_data_graph(data, title, x_label, y_label):
y_margin = 0.1 * (np.max(data) - np.min(data))
f, ax = plt.subplots(1)
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.title(title)
ax.set_xlim(left=0, right=len(data) + 1)
ax.set_xticks(np.linspace(0, len(data), int(len(data) / 5) + 1))
ax.set_ylim(bottom=np.min(data) - y_margin, top=np.max(data) + y_margin)
ax.plot(range(1, len(data) + 1), data)
plt.show()
2. Define our quantization configuration
The pt2e quantization library is extremely flexible. It allows us to define the method of quantization for each tensor of each individual operation. Here we will define a simple quantization configuration, but you may choose to experiment with the options available.
sym_fake_quant = FakeQuantize.with_args(
observer=MovingAverageMinMaxObserver,
quant_min=-128,
quant_max=127,
dtype=torch.qint8,
qscheme=torch.per_tensor_symmetric,
)
sym_spec = QuantizationSpec(
observer_or_fake_quant_ctr=sym_fake_quant,
dtype=torch.int8,
quant_min=-128,
quant_max=127,
qscheme=torch.per_tensor_symmetric,
)
class QuadricQuantizer(Quantizer):
def __init__(self):
super().__init__()
def annotate(self, model: torch.fx.GraphModule) -> torch.fx.GraphModule:
self._annotate_add(model)
self._annotate_conv2d(model)
self._annotate_linear(model)
self._annotate_matmul(model)
self._annotate_average_pool(model)
return model
def _annotate_add(self, model: torch.fx.GraphModule):
add_partitions = get_source_partitions(
model.graph, [operator.add, torch.add, torch.Tensor.add, torch.Tensor.add_]
)
add_partitions = list(itertools.chain(*add_partitions.values()))
for partition in add_partitions:
add_node = partition.output_nodes[0]
if "unsqueeze" not in add_node.args[1].name:
continue
# Skip mask adds (5D tensor) - only quantize bias adds (4D tensor)
# Mask adds have shape like (1, 64, 1, 49, 49) for attention mask
# Bias adds have shape like (1, 4, 49, 49) for relative position bias
arg1_meta = add_node.args[1].meta.get("tensor_meta") or add_node.args[1].meta.get("val")
if arg1_meta is not None and hasattr(arg1_meta, "shape"):
if len(arg1_meta.shape) == 5:
continue
input_qspec_map = {
add_node.args[0]: sym_spec,
add_node.args[1]: sym_spec,
}
add_node.meta["quantization_annotation"] = QuantizationAnnotation(
input_qspec_map=input_qspec_map,
output_qspec=sym_spec,
_annotated=True,
)
def _annotate_conv2d(self, model: torch.fx.GraphModule):
conv_partitions = get_source_partitions(model.graph, [nn.Conv2d, F.conv2d])
conv_partitions = list(itertools.chain(*conv_partitions.values()))
for partition in conv_partitions:
conv_node = partition.output_nodes[0]
input_qspec_map = {
conv_node.args[0]: sym_spec,
conv_node.args[1]: sym_spec,
}
conv_node.meta["quantization_annotation"] = QuantizationAnnotation(
input_qspec_map=input_qspec_map,
output_qspec=sym_spec,
_annotated=True,
)
def _annotate_linear(self, model: torch.fx.GraphModule):
linear_partitions = get_source_partitions(model.graph, [nn.Linear, F.linear])
linear_partitions = list(itertools.chain(*linear_partitions.values()))
for partition in linear_partitions:
linear_node = partition.output_nodes[0]
input_qspec_map = {
linear_node.args[0]: sym_spec,
linear_node.args[1]: sym_spec,
}
linear_node.meta["quantization_annotation"] = QuantizationAnnotation(
input_qspec_map=input_qspec_map,
output_qspec=sym_spec,
_annotated=True,
)
def _annotate_matmul(self, model: torch.fx.GraphModule):
matmul_partitions = get_source_partitions(
model.graph, [operator.matmul, torch.matmul, torch.mm]
)
matmul_partitions = list(itertools.chain(*matmul_partitions.values()))
for partition in matmul_partitions:
matmul_node = partition.output_nodes[0]
input_qspec_map = {
matmul_node.args[0]: sym_spec,
matmul_node.args[1]: sym_spec,
}
matmul_node.meta["quantization_annotation"] = QuantizationAnnotation(
input_qspec_map=input_qspec_map,
output_qspec=sym_spec,
_annotated=True,
)
def _annotate_average_pool(self, model: torch.fx.GraphModule):
gap_partitions = get_source_partitions(
model.graph,
[nn.AvgPool1d, nn.AdaptiveAvgPool1d, F.avg_pool1d, F.adaptive_avg_pool1d],
)
gap_partitions = list(itertools.chain(*gap_partitions.values()))
for partition in gap_partitions:
gap_node = partition.output_nodes[0]
input_qspec_map = {
gap_node.args[0]: sym_spec,
}
gap_node.meta["quantization_annotation"] = QuantizationAnnotation(
input_qspec_map=input_qspec_map, output_qspec=sym_spec, _annotated=True
)
def validate(self, model):
pass
@classmethod
def get_supported_operators(cls):
return []
3. Prepare the training/evaluation data
We use images from ImageNet-1K to perform the training and evaluation. All input tensors in this dataset are of dimension NCHW format [3, 224, 224] and need to be float-normalized with a mean, sigma of [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]. For training, we include the transformations RandomResizedCrop, RandomHorizontalFlip, and RandomVerticalFlip for robustness.
normalize = Normalize(
IMAGENET_1K_NORMALIZATION_PARAMETERS.channel_means,
IMAGENET_1K_NORMALIZATION_PARAMETERS.channel_standard_deviations,
)
to_tensor = Compose([ToImage(), ToDtype(torch.float32, scale=True)])
train_transforms = Compose(
[
RandomResizedCrop(224),
RandomHorizontalFlip(),
RandomVerticalFlip(),
to_tensor,
normalize,
]
)
test_transforms = Compose(
[
Resize(224),
CenterCrop(224),
to_tensor,
normalize,
]
)
def apply_transforms(imgs):
imgs["image"] = [train_transforms(img.convert("RGB")) for img in imgs["image"]]
return imgs
train_set = load_from_disk(
"s3://sdk-cli-datasets/imagenet_1k_val_train.hf/validation",
storage_options={"anon": True},
)
train_set.set_transform(apply_transforms)
test_set = ImageFolder(
root="../../common/validation/imagenet-mini-quadric/val", transform=test_transforms
)
train_batch_size = 1
test_batch_size = 1
generator_train = torch.Generator(device="cuda")
generator_train.manual_seed(67280421310721)
generator_test = torch.Generator(device="cuda")
generator_test.manual_seed(672804213107421)
sampler_train = torch.utils.data.RandomSampler(train_set, generator=generator_train)
sampler_test = torch.utils.data.SequentialSampler(test_set)
data_loader_train = torch.utils.data.DataLoader(
train_set,
batch_size=train_batch_size,
generator=generator_train,
sampler=sampler_train,
)
data_loader_test = torch.utils.data.DataLoader(
test_set,
batch_size=test_batch_size,
generator=generator_test,
sampler=sampler_test,
)
/usr/local/lib/python3.10/dist-packages/datasets/table.py:1421: FutureWarning: promote has been superseded by promote_options='default'.
table = cls._concat_blocks(blocks, axis=0)
4. Load Swin from official Microsoft repository
Here we use the swin_base_patch4_window7_224_22kto1k model. The original graph goes through a 2-step process using torch.export.export_for_training and the previously defined QuadricQuantizer in order to be ready for QAT.
basename = Path("Swin-Transformer")
## Install Swin-Transformer.
if basename.exists():
print(f"Existing {basename} is used.")
else:
url = "https://github.com/microsoft/Swin-Transformer/archive/refs/heads/main.zip"
filename = Path(url).name
download_file(url, filename)
with zipfile.ZipFile(filename, "r") as fd:
fd.extractall()
os.rename(f"{basename}-{Path(url).stem}", basename)
os.remove(filename)
print(f"{basename} is installed.")
## Make symbolic links for later experiment.
symlink_list = ["configs", "models", "config.py", "logger.py", "utils.py"]
for sym_path in symlink_list:
if not Path(sym_path).is_symlink():
os.symlink(basename / sym_path, Path(sym_path).name)
print(f"{sym_path} is symbolic")
Swin-Transformer is installed.
configs is symbolic
models is symbolic
config.py is symbolic
logger.py is symbolic
utils.py is symbolic
MODEL_NAME = "swin_base_patch4_window7_224_22kto1k"
weights_url = (
f"https://github.com/SwinTransformer/storage/releases/download/v1.0.0/{MODEL_NAME}.pth"
)
config = f"configs/swin/{MODEL_NAME}_finetune.yaml"
weights = Path(weights_url).name
if not Path(weights).exists():
download_file(weights_url, weights)
print(f"{weights} is downloaded.")
swin_base_patch4_window7_224_22kto1k.pth is downloaded.
from config import get_config
from models import build_model
from logger import create_logger
from utils import load_pretrained
class swin_args:
def __init__(self):
self.cfg = config
self.opts = []
self.pretrained = weights
os.environ["LOCAL_RANK"] = "0"
args = swin_args()
config = get_config(args)
os.makedirs(config.OUTPUT, exist_ok=True)
logger = create_logger(output_dir=config.OUTPUT, name=f"{config.MODEL.NAME}")
logger.info(f"Creating model:{config.MODEL.TYPE}/{config.MODEL.NAME}")
float_model = build_model(config)
n_parameters = sum(p.numel() for p in float_model.parameters() if p.requires_grad)
logger.info(f"number of params: {n_parameters}")
if hasattr(float_model, "flops"):
flops = float_model.flops()
logger.info(f"number of GFLOPs: {flops / 1e9}")
load_pretrained(config, float_model, logger)
[Warning] Fused window process have not been installed. Please refer to get_started.md for installation.
Tutel has not been installed. To use Swin-MoE, please install Tutel; otherwise, just ignore this.
=> merge config from configs/swin/swin_base_patch4_window7_224_22kto1k_finetune.yaml
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](2345668462.py 22): INFO Creating model:swin/swin_base_patch4_window7_224_22kto1k_finetune
/usr/local/lib/python3.10/dist-packages/torch/functional.py:534: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument. (Triggered internally at ../aten/src/ATen/native/TensorShape.cpp:3595.)
return _VF.meshgrid(tensors, **kwargs) # type: ignore[attr-defined]
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](2345668462.py 26): INFO number of params: 87768224
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](2345668462.py 29): INFO number of GFLOPs: 15.438473216
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](utils.py 46): INFO ==============> Loading weight swin_base_patch4_window7_224_22kto1k.pth for fine-tuning......
/quadric/sdk-cli/examples/models/swin/utils.py:47: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.
checkpoint = torch.load(config.MODEL.PRETRAINED, map_location='cpu')
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](utils.py 127): WARNING _IncompatibleKeys(missing_keys=['layers.0.blocks.0.attn.relative_position_index', 'layers.0.blocks.1.attn_mask', 'layers.0.blocks.1.attn.relative_position_index', 'layers.1.blocks.0.attn.relative_position_index', 'layers.1.blocks.1.attn_mask', 'layers.1.blocks.1.attn.relative_position_index', 'layers.2.blocks.0.attn.relative_position_index', 'layers.2.blocks.1.attn_mask', 'layers.2.blocks.1.attn.relative_position_index', 'layers.2.blocks.2.attn.relative_position_index', 'layers.2.blocks.3.attn_mask', 'layers.2.blocks.3.attn.relative_position_index', 'layers.2.blocks.4.attn.relative_position_index', 'layers.2.blocks.5.attn_mask', 'layers.2.blocks.5.attn.relative_position_index', 'layers.2.blocks.6.attn.relative_position_index', 'layers.2.blocks.7.attn_mask', 'layers.2.blocks.7.attn.relative_position_index', 'layers.2.blocks.8.attn.relative_position_index', 'layers.2.blocks.9.attn_mask', 'layers.2.blocks.9.attn.relative_position_index', 'layers.2.blocks.10.attn.relative_position_index', 'layers.2.blocks.11.attn_mask', 'layers.2.blocks.11.attn.relative_position_index', 'layers.2.blocks.12.attn.relative_position_index', 'layers.2.blocks.13.attn_mask', 'layers.2.blocks.13.attn.relative_position_index', 'layers.2.blocks.14.attn.relative_position_index', 'layers.2.blocks.15.attn_mask', 'layers.2.blocks.15.attn.relative_position_index', 'layers.2.blocks.16.attn.relative_position_index', 'layers.2.blocks.17.attn_mask', 'layers.2.blocks.17.attn.relative_position_index', 'layers.3.blocks.0.attn.relative_position_index', 'layers.3.blocks.1.attn.relative_position_index'], unexpected_keys=[])
[2026-06-19 06:21:07 swin_base_patch4_window7_224_22kto1k_finetune](utils.py 129): INFO => loaded successfully 'swin_base_patch4_window7_224_22kto1k.pth'
example_input = torch.rand(1, 3, 224, 224)
exported_model = torch.export.export_for_training(float_model, (example_input,)).module()
quantizer = QuadricQuantizer()
prepared_model = prepare_qat_pt2e(exported_model, quantizer)
for n in prepared_model.graph.nodes:
if n.target == torch.ops.aten._native_batch_norm_legit.default:
n.target = torch.ops.aten.cudnn_batch_norm.default
_ = prepared_model.recompile()
_ = float_model.eval()
/usr/local/lib/python3.10/dist-packages/onnxscript/converter.py:823: FutureWarning: 'onnxscript.values.Op.param_schemas' is deprecated in version 0.1 and will be removed in the future. Please use '.op_signature' instead.
param_schemas = callee.param_schemas()
/usr/local/lib/python3.10/dist-packages/onnxscript/converter.py:823: FutureWarning: 'onnxscript.values.OnnxFunction.param_schemas' is deprecated in version 0.1 and will be removed in the future. Please use '.op_signature' instead.
param_schemas = callee.param_schemas()
5. Perform QAT using the pt2e library
We follow the documentation given here. We start by defining some hyperparameters here, which can be altered to experiment with different setups.
num_epochs = 40
num_train_batches = 32
num_eval_batches = 1500
final_num_eval_batches = 3500
num_epochs_between_evals = 10
num_observer_update_epochs = 1000
num_batch_norm_update_epochs = 0
num_warmup_epochs = 5
temp = 3
kd_scale = 0.3
checkpoints = []
lr_data = []
loss_data = []
eval_data = []
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(prepared_model.parameters(), lr=2e-6, weight_decay=1e-5, amsgrad=True)
warmup_scheduler = torch.optim.lr_scheduler.LinearLR(
optimizer, start_factor=1e-3, total_iters=num_warmup_epochs
)
cosine_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=num_epochs - num_warmup_epochs, eta_min=1e-9
)
scheduler = torch.optim.lr_scheduler.SequentialLR(
optimizer,
schedulers=[warmup_scheduler, cosine_scheduler],
milestones=[num_warmup_epochs],
)
num_observer_update_flag = True
num_batch_norm_update_flag = True
for epoch in range(num_epochs):
if epoch + 1 >= num_observer_update_epochs and num_observer_update_flag:
print("Disabling observer for subseq epochs, epoch = ", epoch)
prepared_model.apply(torch.ao.quantization.disable_observer)
num_observer_update_flag = False
if epoch + 1 >= num_batch_norm_update_epochs and num_batch_norm_update_flag:
print("Freezing BN for subseq epochs, epoch = ", epoch)
for n in prepared_model.graph.nodes:
if n.target in [
torch.ops.aten._native_batch_norm_legit.default,
torch.ops.aten.cudnn_batch_norm.default,
]:
new_args = list(n.args)
new_args[5] = False
n.args = tuple(new_args)
prepared_model.recompile()
num_batch_norm_update_flag = False
train_one_epoch_distill(
float_model,
prepared_model,
criterion,
optimizer,
temp,
kd_scale,
loss_data,
data_loader_train,
num_train_batches,
)
print("^ Epoch: %d" % (epoch + 1))
current_lr = optimizer.param_groups[0]["lr"]
lr_data.append(current_lr)
print(f"^ LR = {current_lr:.8f}")
if (epoch + 1) % num_epochs_between_evals == 0:
checkpoints.append(copy.deepcopy(prepared_model.state_dict()))
prepared_model_copy = copy.deepcopy(prepared_model)
quantized_model = convert_pt2e(prepared_model_copy)
torch.ao.quantization.move_exported_model_to_eval(quantized_model)
top1, top5 = evaluate(
quantized_model,
data_loader_test,
neval_batches=num_eval_batches,
)
eval_data.append(top1.avg.item())
print(
"Epoch %d: Evaluation accuracy on %d images, %2.2f\n"
% (epoch + 1, num_eval_batches * test_batch_size, top1.avg)
)
del prepared_model_copy
del quantized_model
scheduler.step()
Freezing BN for subseq epochs, epoch = 0
................................Loss tensor(1.6335, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 87.500
^ Epoch: 1
^ LR = 0.00000000
................................Loss tensor(1.3526, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 90.625
^ Epoch: 2
^ LR = 0.00000040
................................Loss tensor(1.1511, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 96.875
^ Epoch: 3
^ LR = 0.00000080
................................Loss tensor(1.0684, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 84.375 Acc@5 93.750
^ Epoch: 4
^ LR = 0.00000120
................................Loss tensor(1.1151, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 93.750
^ Epoch: 5
^ LR = 0.00000160
/usr/local/lib/python3.10/dist-packages/torch/optim/lr_scheduler.py:240: UserWarning: The epoch parameter in `scheduler.step()` was not necessary and is being deprecated where possible. Please use `scheduler.step()` to step the scheduler. During the deprecation, if epoch is different from None, the closed form is used instead of the new chainable form, where available. Please open an issue if you are unable to replicate your use case: https://github.com/pytorch/pytorch/issues/new/choose.
warnings.warn(EPOCH_DEPRECATION_WARNING, UserWarning)
................................Loss tensor(1.8048, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 84.375
^ Epoch: 6
^ LR = 0.00000200
................................Loss tensor(1.3107, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 81.250 Acc@5 90.625
^ Epoch: 7
^ LR = 0.00000200
................................Loss tensor(0.8758, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 84.375 Acc@5 93.750
^ Epoch: 8
^ LR = 0.00000198
................................Loss tensor(1.7778, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 87.500
^ Epoch: 9
^ LR = 0.00000196
................................Loss tensor(0.9489, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 81.250 Acc@5 100.000
^ Epoch: 10
^ LR = 0.00000194
W0619 06:22:30.514000 30879 torch/_export/__init__.py:64] +============================+
W0619 06:22:30.514000 30879 torch/_export/__init__.py:65] | !!! WARNING !!! |
W0619 06:22:30.515000 30879 torch/_export/__init__.py:66] +============================+
W0619 06:22:30.515000 30879 torch/_export/__init__.py:67] capture_pre_autograd_graph() is deprecated and doesn't provide any function guarantee moving forward.
W0619 06:22:30.515000 30879 torch/_export/__init__.py:68] Please switch to use torch.export.export_for_training instead.
Epoch 10: Evaluation accuracy on 1500 images, 81.27
................................Loss tensor(1.6679, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 68.750 Acc@5 84.375
^ Epoch: 11
^ LR = 0.00000190
................................Loss tensor(1.3037, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 68.750 Acc@5 93.750
^ Epoch: 12
^ LR = 0.00000186
................................Loss tensor(1.4886, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 87.500
^ Epoch: 13
^ LR = 0.00000181
................................Loss tensor(1.5117, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 71.875 Acc@5 84.375
^ Epoch: 14
^ LR = 0.00000175
................................Loss tensor(1.6722, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 78.125
^ Epoch: 15
^ LR = 0.00000169
................................Loss tensor(1.3512, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 84.375
^ Epoch: 16
^ LR = 0.00000162
................................Loss tensor(1.6839, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 84.375
^ Epoch: 17
^ LR = 0.00000155
................................Loss tensor(1.7368, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 84.375
^ Epoch: 18
^ LR = 0.00000147
................................Loss tensor(1.5558, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 90.625
^ Epoch: 19
^ LR = 0.00000139
................................Loss tensor(1.1229, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 78.125 Acc@5 93.750
^ Epoch: 20
^ LR = 0.00000131
Epoch 20: Evaluation accuracy on 1500 images, 81.73
................................Loss tensor(1.4364, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 71.875 Acc@5 90.625
^ Epoch: 21
^ LR = 0.00000122
................................Loss tensor(1.0169, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 93.750
^ Epoch: 22
^ LR = 0.00000113
................................Loss tensor(1.6169, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 81.250
^ Epoch: 23
^ LR = 0.00000105
................................Loss tensor(1.1480, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 90.625
^ Epoch: 24
^ LR = 0.00000096
................................Loss tensor(1.5082, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 68.750 Acc@5 87.500
^ Epoch: 25
^ LR = 0.00000087
................................Loss tensor(1.9860, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 59.375 Acc@5 78.125
^ Epoch: 26
^ LR = 0.00000078
................................Loss tensor(1.3548, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 93.750
^ Epoch: 27
^ LR = 0.00000069
................................Loss tensor(1.8283, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 78.125
^ Epoch: 28
^ LR = 0.00000061
................................Loss tensor(1.8307, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 56.250 Acc@5 84.375
^ Epoch: 29
^ LR = 0.00000053
................................Loss tensor(1.8659, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 53.125 Acc@5 78.125
^ Epoch: 30
^ LR = 0.00000045
Epoch 30: Evaluation accuracy on 1500 images, 81.67
................................Loss tensor(0.9725, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 78.125 Acc@5 96.875
^ Epoch: 31
^ LR = 0.00000038
................................Loss tensor(1.3465, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 78.125 Acc@5 84.375
^ Epoch: 32
^ LR = 0.00000031
................................Loss tensor(1.7764, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 87.500
^ Epoch: 33
^ LR = 0.00000025
................................Loss tensor(0.8728, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 84.375 Acc@5 96.875
^ Epoch: 34
^ LR = 0.00000019
................................Loss tensor(1.5865, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 62.500 Acc@5 84.375
^ Epoch: 35
^ LR = 0.00000014
................................Loss tensor(0.9351, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 81.250 Acc@5 93.750
^ Epoch: 36
^ LR = 0.00000010
................................Loss tensor(1.9387, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 65.625 Acc@5 75.000
^ Epoch: 37
^ LR = 0.00000006
................................Loss tensor(1.1755, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 71.875 Acc@5 93.750
^ Epoch: 38
^ LR = 0.00000004
................................Loss tensor(0.8961, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 75.000 Acc@5 96.875
^ Epoch: 39
^ LR = 0.00000002
................................Loss tensor(1.5234, device='cuda:0', grad_fn=<DivBackward0>)
Training: * Acc@1 71.875 Acc@5 90.625
^ Epoch: 40
^ LR = 0.00000001
Epoch 40: Evaluation accuracy on 1500 images, 83.20
6. ONNX export and post-processing
We use torch.onnx.dynamo_export() to export the graph from PyTorch to ONNX and use Quadric's QAT post-processor in order to clean up the graph and format it nicely. We start here with the conversion from PyTorch to ONNX.
<div class="alert alert-block alert-warning"> <b>Note:</b> Because torch.onnx.dynamo_export is a work in progress, there will be many warnings generated for now. There are no adverse effects. These are just remnants from other functionalities, so they can be ignored. But they cannot be suppressed as they are thrown by the underlying C++. </div>
print_data_graph(lr_data, "LR data", "Epochs", "LR")
print_data_graph(loss_data, "Loss data", "Epochs", "CCE Loss")
print_data_graph(eval_data, "Eval data", f"Epochs / {num_epochs_between_evals}", "T1 Accuracy")
quantized_model = convert_pt2e(prepared_model)
torch.ao.quantization.move_exported_model_to_eval(quantized_model)
top1, top5 = evaluate(quantized_model, data_loader_test, neval_batches=final_num_eval_batches)
print(
"Final evaluation accuracy on %d images, (Top-1): %2.2f"
% (final_num_eval_batches * test_batch_size, top1.avg)
)
print(
"Final evaluation accuracy on %d images, (Top-5): %2.2f"
% (final_num_eval_batches * test_batch_size, top5.avg)
)
onnx_program = torch.onnx.dynamo_export(quantized_model, example_input)
onnx_program.save("swin-unprocessed.onnx")



Final evaluation accuracy on 3500 images, (Top-1): 80.89
Final evaluation accuracy on 3500 images, (Top-5): 95.89
/usr/local/lib/python3.10/dist-packages/torch/onnx/_internal/_exporter_legacy.py:116: UserWarning: torch.onnx.dynamo_export only implements opset version 18 for now. If you need to use a different opset version, please register them with register_custom_op.
warnings.warn(
/usr/local/lib/python3.10/dist-packages/torch/onnx/_internal/fx/passes/readability.py:52: UserWarning: Attempted to insert a get_attr Node with no underlying reference in the owning GraphModule! Call GraphModule.add_submodule to add the necessary submodule, GraphModule.add_parameter to add the necessary Parameter, or nn.Module.register_buffer to add the necessary buffer
new_node = self.module.graph.get_attr(normalized_name)
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1586: UserWarning: Node _frozen_param0 target _frozen_param0 _frozen_param0 of does not reference an nn.Module, nn.Parameter, or buffer, which is what 'get_attr' Nodes typically target
warnings.warn(f'Node {node} target {node.target} {atom} of {seen_qualname} does '
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1586: UserWarning: Node _frozen_param1 target _frozen_param1 _frozen_param1 of does not reference an nn.Module, nn.Parameter, or buffer, which is what 'get_attr' Nodes typically target
warnings.warn(f'Node {node} target {node.target} {atom} of {seen_qualname} does '
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1586: UserWarning: Node _frozen_param2 target _frozen_param2 _frozen_param2 of does not reference an nn.Module, nn.Parameter, or buffer, which is what 'get_attr' Nodes typically target
warnings.warn(f'Node {node} target {node.target} {atom} of {seen_qualname} does '
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1586: UserWarning: Node _frozen_param3 target _frozen_param3 _frozen_param3 of does not reference an nn.Module, nn.Parameter, or buffer, which is what 'get_attr' Nodes typically target
warnings.warn(f'Node {node} target {node.target} {atom} of {seen_qualname} does '
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1586: UserWarning: Node _frozen_param4 target _frozen_param4 _frozen_param4 of does not reference an nn.Module, nn.Parameter, or buffer, which is what 'get_attr' Nodes typically target
warnings.warn(f'Node {node} target {node.target} {atom} of {seen_qualname} does '
/usr/local/lib/python3.10/dist-packages/torch/fx/graph.py:1593: UserWarning: Additional 131 warnings suppressed about get_attr references
warnings.warn(
/usr/local/lib/python3.10/dist-packages/torch/onnx/_internal/fx/onnxfunction_dispatcher.py:503: FutureWarning: 'onnxscript.values.TracedOnnxFunction.param_schemas' is deprecated in version 0.1 and will be removed in the future. Please use '.op_signature' instead.
self.param_schema = self.onnxfunction.param_schemas()
Skipping constant folding for op SequenceEmpty with multiple outputs.
Applied 171 of general pattern rewrite rules.
Skipping constant folding for op SequenceEmpty with multiple outputs.
The next step uses Quadric's PyTorch graph post-processor. We avoid quantizing residual adds here for accuracy preservation
Handled ops/patterns:
- Conv (+ BatchNormalization + activation)
- Gemm
- MatMul
- Add
- GlobalAveragePool
- MHA (limited to ViT and Swin examples)
aten_bernoulli_paten_gelu_approximate_noneaten_roll_shift_and_dimaten_as_stridedaten_constant_pad_nd
torch.set_default_device("cpu")
output_path = "./swin-processed.onnx"
qat_processor = processor.QATProcessor()
qat_processor.process(
"swin-unprocessed.onnx",
ptq_sym_act=True,
strip_onnx=False,
output_path=output_path,
)
2026-06-19 06:33 - DEBUG - epu - qat_processor - Preparing calibration data
/usr/local/lib/python3.10/dist-packages/datasets/table.py:1421: FutureWarning: promote has been superseded by promote_options='default'.
table = cls._concat_blocks(blocks, axis=0)
2026-06-19 06:33 - DEBUG - epu - qat_processor - Fixing PyTorch exported Unsqueeze axes arg
2026-06-19 06:33 - DEBUG - epu - qat_processor - Removing aten.bernoulli ops
2026-06-19 06:33 - DEBUG - epu - qat_processor - Removing NOP aten.pad ops
2026-06-19 06:33 - DEBUG - epu - qat_processor - Removing aten.as_strided ops
2026-06-19 06:33 - DEBUG - epu - qat_processor - Swapping aten.gelu ops for ONNX equivalent
2026-06-19 06:33 - DEBUG - epu - qat_processor - Swapping aten.roll ops for ONNX equivalent
2026-06-19 06:33 - DEBUG - epu - qat_processor - Swapping ReduceMean for GlobalAveragePool
2026-06-19 06:33 - DEBUG - epu - qat_processor - Removing Dropout ops with rate=0
2026-06-19 06:33 - DEBUG - epu - qat_processor - Removing redundant QDQs
2026-06-19 06:33 - DEBUG - epu - qat_processor - Folding Div into BatchNormalization
2026-06-19 06:33 - DEBUG - epu - qat_processor - Folding BatchNormalization into Conv nodes with biases
2026-06-19 06:33 - DEBUG - epu - qat_processor - Folding BatchNormalization into Conv nodes without biases
2026-06-19 06:33 - DEBUG - epu - qat_processor - Quantizing Swin MHA blocks
WARNING:root:Please use QuantFormat.QDQ for activation type QInt8 and weight type QInt8. Or it will lead to bad performance on x64.
WARNING:root:Please check if the model is already quantized. Note you don't need to quantize a QAT model. OnnxRuntime support to run QAT model directly.
2026-06-19 06:39 - DEBUG - epu - qat_processor - Quantizing FC layers
WARNING:root:Please use QuantFormat.QDQ for activation type QInt8 and weight type QInt8. Or it will lead to bad performance on x64.
WARNING:root:Please check if the model is already quantized. Note you don't need to quantize a QAT model. OnnxRuntime support to run QAT model directly.
2026-06-19 06:46 - DEBUG - epu - qat_processor - Quantizing QKV MatMul ops
2026-06-19 06:46 - DEBUG - epu - qat_processor - Changing to Opset 17
2026-06-19 06:46 - DEBUG - epu - qat_processor - Performing ORT optimizations
2026-06-19 06:46 - DEBUG - epu - qat_processor - Unfolding QLinearSoftmax to avoid inaccurate LUT
2026-06-19 06:46 - DEBUG - epu - qat_processor - Fusing QLinearConv
2026-06-19 06:46 - DEBUG - epu - qat_processor - Fusing QGemm
2026-06-19 06:46 - DEBUG - epu - qat_processor - Folding DQ into MatMul ops where ORT does not
2026-06-19 06:46 - DEBUG - epu - qat_processor - Moving DQ outside of Roll patterns
2026-06-19 06:46 - DEBUG - epu - qat_processor - Moving from uint8 to int8
7. Prepare the validation dataset
Now that the QAT model has been saved to disk, we will compare it to the original pretrained FP32 model from PyTorch. Here we use a set of 3500 images in our validation of the model.
torch.set_default_device("cpu")
MAX_NUM_SAMPLES_FOR_ACCURACY_CALCULATION = 3500
graph_input_name = "l_x_"
assert isinstance(graph_input_name, str)
normalize = Normalize(
IMAGENET_1K_NORMALIZATION_PARAMETERS.channel_means,
IMAGENET_1K_NORMALIZATION_PARAMETERS.channel_standard_deviations,
)
to_tensor = Compose([ToImage(), ToDtype(torch.float32, scale=True)])
test_transforms = Compose(
[
Resize(224),
CenterCrop(224),
to_tensor,
normalize,
]
)
dataset = ImageNet_Mini_Quadric.Dataset(transform=test_transforms)
subset_of_dataset = Subset(dataset, range(MAX_NUM_SAMPLES_FOR_ACCURACY_CALCULATION))
calibration_dataloader = CalibrationDataLoader(
DataLoader(subset_of_dataset, batch_size=1, shuffle=True), [graph_input_name]
)
8. Validate and compare against FP32 model
Now we want to make sure that quantization was done succesfully by checking the performance of the original model against the quantized model.
fp32_path = "./swin_base_float32.onnx"
int8_path = output_path
torch.onnx.export(
float_model,
example_input,
fp32_path,
input_names=[graph_input_name],
export_params=True,
opset_version=17,
)
onnx_model_path = Path(fp32_path)
quantized_onnx_model = QuantizedONNXModel(int8_path, None)
## Evaluation
quantization_experiment = QuantizationExperiment(
floating_point_onnx_model_path=onnx_model_path,
quantized_onnx_model=quantized_onnx_model,
performance_tracker=ClassifierPerformanceTracker(),
disable_opt=True,
)
run_quantization_experiment(
quantization_experiment,
calibration_dataloader,
export_path=None,
max_num_samples=MAX_NUM_SAMPLES_FOR_ACCURACY_CALCULATION,
)
3500: FP3284.03% <> INT877.83%: 100%|██████████| 3500/3500 [17:59<00:00, 3.24it/s]
Used 3500 image samples for model accuracy comparison.
Original FP32 model accuracy (Top-1): 84.03%
Quantized INT8 model accuracy (Top-1): 77.83%
Change in Top-1 model accuracy due to quantization: 6.20%
9. Lower the graph in CGC
We first create the tensor-ranges file, then lower the graph.
warnings.filterwarnings(action="ignore")
tvm_logger.setLevel(logging.WARNING)
MAX_NUM_SAMPLES_FOR_CALIBRATION = 1000
calibration_dataset = ImageNet_Mini_Quadric.Dataset(transform=test_transforms)
calibration_dataset = Subset(calibration_dataset, range(MAX_NUM_SAMPLES_FOR_CALIBRATION))
trange_path = f"{int8_path}.tranges"
_ = compute_tensor_ranges(
onnx_model_path=int8_path,
calibration_dataloader=CalibrationDataLoader(calibration_dataset, [graph_input_name]),
tensor_ranges_export_path=trange_path,
)
2026-06-19 07:09 - INFO - sdk - quantize - Saved computed tensor ranges to ./swin-processed.onnx.tranges.
from tvm.contrib.epu.chimera_job.hw_config import DEFAULT_32_ARRAY_SIZE_8MAC
hw_cfg = DEFAULT_32_ARRAY_SIZE_8MAC
cgc_job = ChimeraJob(
hw_config=hw_cfg,
model_p=int8_path,
onnx_ort_override_p=int8_path,
trange_file=trange_path,
)
from tvm.relay.backend.contrib.epu.directives import generate_patch_merge_directives
## Add directives to skip quantizing patch merge
span_directives, _ = generate_patch_merge_directives(int8_path)
cgc_job.span_directives = span_directives
cgc_job.compile(quiet=True)
print(cgc_job)
╒═════════════════════╤══════════════════════════════════════════════════════════════╕
│ Module Name │ swin_processed_QC_U_1d7_16MB_4kB_128GBps_128GBps_8_OFF_x1_x1 │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ ONNX File │ ./swin-processed.onnx │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Product Target │ QC-U │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Number of Cores │ 1 │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ ISS Clock Frequency │ 1.700 │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ L2M Size │ 16MB │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ LRM Size │ 4kB │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ External Read BW │ 128GBps │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ External Write BW │ 128GBps │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ MACS per PE │ 8 │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Max L2M │ 14.839MB │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Max LRM │ 3.000kB │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Max Temp Ext Bytes │ 0.000MB │
├─────────────────────┼──────────────────────────────────────────────────────────────┤
│ Network GMACs │ 15.431 │
╘═════════════════════╧══════════════════════════════════════════════════════════════╛
╒════╤════════╤═══════════════════════════╤══════════════════╤══════════════════════════╤═══════╕
│ │ Type │ Name │ shape │ type │ mse │
╞════╪════════╪═══════════════════════════╪══════════════════╪══════════════════════════╪═══════╡
│ 0 │ Input │ l_x_ │ [1, 3, 224, 224] │ tensor[FixedPoint32<29>] │ n/a │
├────┼────────┼───────────────────────────┼──────────────────┼──────────────────────────┼───────┤
│ 1 │ Output │ dequantize_per_tensor_496 │ [1, 1000] │ tensor[FixedPoint32<27>] │ n/a │
╘════╧════════╧═══════════════════════════╧══════════════════╧══════════════════════════╧═══════╛
10. Demo
We first run inference, then show the statistics collected.
from sdk_cli.lib.inference import InferenceEngine, batch_inference
IMAGENET_DATA_DIR = Path("../../common/validation/imagenet-mini-quadric/val")
all_images_dict = {
str(IMAGENET_DATA_DIR / "n03777568" / "ILSVRC2012_val_00049034.JPEG"): 661,
str(IMAGENET_DATA_DIR / "n02877765" / "ILSVRC2012_val_00049979.JPEG"): 455,
str(IMAGENET_DATA_DIR / "n02125311" / "ILSVRC2012_val_00015558.JPEG"): 286,
}
all_image_paths = list(all_images_dict.keys())
NUM_IMAGES = len(all_image_paths)
engines = {
InferenceEngine.CHIMERA_ORT_INT8: cgc_job,
InferenceEngine.CHIMERA_ISS_INT8: cgc_job,
}
all_images = []
for image_path in all_image_paths:
all_images.append(np.expand_dims(test_transforms(Image.open(image_path)), axis=0))
outputs_per_inference_engine = {}
THREADS = min(NUM_IMAGES, 6)
for inference_engine, engine in engines.items():
outputs_per_inference_engine[inference_engine] = batch_inference(
inference_engine,
engine,
all_images,
threads=THREADS,
)
2026-06-19 07:19 - WARNING - epu - chimera_job - ORT is not threadsafe -- forcing single threaded batch execution
100%|██████████| 3/3 [00:03<00:00, 1.07s/it]
Processing: 100%|██████████| 3/3 [03:44<00:00, 74.74s/it]
from sdk_cli.utils.datasets.ImageNet import OPTIMIZED_IMAGENET_1K_LABELS
from sdk_cli.visualizers.layouter import ClassifierLayouter
%matplotlib inline
for i, image_path in enumerate(all_image_paths):
title = f"{all_images_dict[image_path]}: {OPTIMIZED_IMAGENET_1K_LABELS.class_map[all_images_dict[image_path]]}"
classifier_layouter = ClassifierLayouter(image_path, title)
for inference_engine, all_outputs in outputs_per_inference_engine.items():
classifier_layouter.add_data(all_outputs[i][0], f"Swin Top 5 (%): {str(inference_engine)}")
classifier_layouter.display()



cgc_job.plot_run_statistics();
2026-06-19 07:23 - INFO - epu - chimera_job - Combined plots generated and saved to:
/quadric/sdk-cli/examples/models/swin/ccl_build/swin_processed_QC_U_1d7_16MB_4kB_128GBps_128GBps_8_OFF_x1_x1/run/20260619_071916_f21c35/data/swin_processed_QC_U_1d7_16MB_4kB_128GBps_128GBps_8_OFF_x1_x1.combined.png
