(prototype) PyTorch 2 Export 量化感知訓練 (QAT)¶
建立於:2023 年 10 月 02 日 | 最後更新:2024 年 10 月 23 日 | 最後驗證:2024 年 11 月 05 日
作者:Andrew Or
本教學展示如何在基於 torch.export.export 的圖模式下執行量化感知訓練 (QAT)。 有關 PyTorch 2 Export 量化的一般更多詳細資訊,請參閱後訓練量化教學。
PyTorch 2 Export QAT 流程如下 - 在很大程度上與後訓練量化 (PTQ) 流程相似
import torch
from torch._export import capture_pre_autograd_graph
from torch.ao.quantization.quantize_pt2e import (
prepare_qat_pt2e,
convert_pt2e,
)
from torch.ao.quantization.quantizer.xnnpack_quantizer import (
XNNPACKQuantizer,
get_symmetric_quantization_config,
)
class M(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(5, 10)
def forward(self, x):
return self.linear(x)
example_inputs = (torch.randn(1, 5),)
m = M()
# Step 1. program capture
# This is available for pytorch 2.5+, for more details on lower pytorch versions
# please check `Export the model with torch.export` section
m = torch.export.export_for_training(m, example_inputs).module()
# we get a model with aten ops
# Step 2. quantization-aware training
# backend developer will write their own Quantizer and expose methods to allow
# users to express how they want the model to be quantized
quantizer = XNNPACKQuantizer().set_global(get_symmetric_quantization_config())
m = prepare_qat_pt2e(m, quantizer)
# train omitted
m = convert_pt2e(m)
# we have a model with aten ops doing integer computations when possible
# move the quantized model to eval mode, equivalent to `m.eval()`
torch.ao.quantization.move_exported_model_to_eval(m)
請注意,在程式碼擷取後不允許呼叫 model.eval()
或 model.train()
,因為這些方法不再能正確地變更某些運算(例如 dropout 和批次正規化)的行為。請改用 torch.ao.quantization.move_exported_model_to_eval()
和 torch.ao.quantization.move_exported_model_to_train()
(即將推出)。
定義輔助函數並準備資料集¶
若要使用完整的 ImageNet 資料集執行本教學中的程式碼,請先按照 ImageNet Data 中的指示下載 ImageNet。 將下載的檔案解壓縮到 data_path
資料夾中。
接下來,下載 torchvision resnet18 模型,並將其重新命名為 data/resnet18_pretrained_float.pth
。
我們將從進行必要的匯入、定義一些輔助函數並準備資料開始。這些步驟與靜態 Eager 模式後訓練量化教學中定義的步驟非常相似
import os
import sys
import time
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision
from torchvision import datasets
from torchvision.models.resnet import resnet18
import torchvision.transforms as transforms
# Set up warnings
import warnings
warnings.filterwarnings(
action='ignore',
category=DeprecationWarning,
module=r'.*'
)
warnings.filterwarnings(
action='default',
module=r'torch.ao.quantization'
)
# Specify random seed for repeatable results
_ = torch.manual_seed(191009)
class AverageMeter(object):
"""Computes and stores the average and current value"""
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):
fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})'
return fmtstr.format(**self.__dict__)
def accuracy(output, target, topk=(1,)):
"""
Computes the accuracy over the k top predictions for the specified
values of k.
"""
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 evaluate(model, criterion, data_loader, device):
torch.ao.quantization.move_exported_model_to_eval(model)
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.to(device)
target = target.to(device)
output = model(image)
loss = criterion(output, target)
cnt += 1
acc1, acc5 = accuracy(output, target, topk=(1, 5))
top1.update(acc1[0], image.size(0))
top5.update(acc5[0], image.size(0))
print('')
return top1, top5
def load_model(model_file):
model = resnet18(pretrained=False)
state_dict = torch.load(model_file, weights_only=True)
model.load_state_dict(state_dict)
return model
def print_size_of_model(model):
if isinstance(model, torch.jit.RecursiveScriptModule):
torch.jit.save(model, "temp.p")
else:
torch.jit.save(torch.jit.script(model), "temp.p")
print("Size (MB):", os.path.getsize("temp.p")/1e6)
os.remove("temp.p")
def prepare_data_loaders(data_path):
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
dataset = torchvision.datasets.ImageNet(
data_path, split="train", transform=transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize,
]))
dataset_test = torchvision.datasets.ImageNet(
data_path, split="val", transform=transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize,
]))
train_sampler = torch.utils.data.RandomSampler(dataset)
test_sampler = torch.utils.data.SequentialSampler(dataset_test)
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=train_batch_size,
sampler=train_sampler)
data_loader_test = torch.utils.data.DataLoader(
dataset_test, batch_size=eval_batch_size,
sampler=test_sampler)
return data_loader, data_loader_test
def train_one_epoch(model, criterion, optimizer, data_loader, device, ntrain_batches):
# Note: do not call model.train() here, since this doesn't work on an exported model.
# Instead, call `torch.ao.quantization.move_exported_model_to_train(model)`, which will
# be added in the near future
top1 = AverageMeter('Acc@1', ':6.2f')
top5 = AverageMeter('Acc@5', ':6.2f')
avgloss = AverageMeter('Loss', '1.5f')
cnt = 0
for image, target in data_loader:
start_time = time.time()
print('.', end = '')
cnt += 1
image, target = image.to(device), target.to(device)
output = model(image)
loss = criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
acc1, acc5 = accuracy(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)
print('Training: * Acc@1 {top1.avg:.3f} Acc@5 {top5.avg:.3f}'
.format(top1=top1, top5=top5))
return
print('Full imagenet train set: * Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f}'
.format(top1=top1, top5=top5))
return
data_path = '~/.data/imagenet'
saved_model_dir = 'data/'
float_model_file = 'resnet18_pretrained_float.pth'
train_batch_size = 32
eval_batch_size = 32
data_loader, data_loader_test = prepare_data_loaders(data_path)
example_inputs = (next(iter(data_loader))[0])
criterion = nn.CrossEntropyLoss()
float_model = load_model(saved_model_dir + float_model_file).to("cuda")
使用 torch.export 匯出模型¶
以下是如何使用 torch.export
匯出模型
from torch._export import capture_pre_autograd_graph
example_inputs = (torch.rand(2, 3, 224, 224),)
# for pytorch 2.5+
exported_model = torch.export.export_for_training(float_model, example_inputs).module()
# for pytorch 2.4 and before
# from torch._export import capture_pre_autograd_graph
# exported_model = capture_pre_autograd_graph(model_to_quantize, example_inputs)
# or, to capture with dynamic dimensions:
# for pytorch 2.5+
dynamic_shapes = tuple(
{0: torch.export.Dim("dim")} if i == 0 else None
for i in range(len(example_inputs))
)
exported_model = torch.export.export_for_training(float_model, example_inputs, dynamic_shapes=dynamic_shapes).module()
# for pytorch 2.4 and before
# dynamic_shape API may vary as well
# from torch._export import dynamic_dim
# example_inputs = (torch.rand(2, 3, 224, 224),)
# exported_model = capture_pre_autograd_graph(
# float_model,
# example_inputs,
# constraints=[dynamic_dim(example_inputs[0], 0)],
# )
匯入後端特定的量化器並設定如何量化模型¶
以下程式碼片段說明如何量化模型
from torch.ao.quantization.quantizer.xnnpack_quantizer import (
XNNPACKQuantizer,
get_symmetric_quantization_config,
)
quantizer = XNNPACKQuantizer()
quantizer.set_global(get_symmetric_quantization_config(is_qat=True))
Quantizer
是後端特定的,每個 Quantizer
都將提供自己的方式來允許使用者設定其模型。
注意
請查看我們的教學,其中描述了如何編寫新的 Quantizer
。
準備模型以進行量化感知訓練¶
prepare_qat_pt2e
在模型中的適當位置插入假量化,並執行適當的 QAT「融合」,例如 Conv2d
+ BatchNorm2d
,以獲得更好的訓練準確度。融合運算表示為已準備圖中的 ATen 運算子圖。
prepared_model = prepare_qat_pt2e(exported_model, quantizer)
print(prepared_model)
注意
如果您的模型包含批次正規化,則您在圖中獲得的實際 ATen 運算子取決於匯出模型時模型的裝置。如果模型位於 CPU 上,則您將獲得 torch.ops.aten._native_batch_norm_legit
。如果模型位於 CUDA 上,則您將獲得 torch.ops.aten.cudnn_batch_norm
。但是,這不是根本性的,並且將來可能會發生變更。
在這兩個運算子之間,已證明 torch.ops.aten.cudnn_batch_norm
在像 MobileNetV2 這樣的模型上提供更好的數值。若要取得此運算子,請在匯出前呼叫 model.cuda()
,或在準備後執行以下命令以手動交換運算子
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()
將來,我們計劃整合批次正規化運算子,以便不再需要上述操作。
訓練迴圈¶
訓練迴圈與先前 QAT 版本中的迴圈類似。為了獲得更好的準確度,您可以選擇在一定數量的 epoch 後停用觀察器並停止更新批次正規化統計資訊,或每隔 N
個 epoch 評估 QAT 模型或目前已訓練的量化模型。
num_epochs = 10
num_train_batches = 20
num_eval_batches = 20
num_observer_update_epochs = 4
num_batch_norm_update_epochs = 3
num_epochs_between_evals = 2
# QAT takes time and one needs to train over a few epochs.
# Train and check accuracy after each epoch
for nepoch in range(num_epochs):
train_one_epoch(prepared_model, criterion, optimizer, data_loader, "cuda", num_train_batches)
# Optionally disable observer/batchnorm stats after certain number of epochs
if epoch >= num_observer_update_epochs:
print("Disabling observer for subseq epochs, epoch = ", epoch)
prepared_model.apply(torch.ao.quantization.disable_observer)
if epoch >= num_batch_norm_update_epochs:
print("Freezing BN for subseq epochs, epoch = ", epoch)
for n in prepared_model.graph.nodes:
# Args: input, weight, bias, running_mean, running_var, training, momentum, eps
# We set the `training` flag to False here to freeze BN stats
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 = new_args
prepared_model.recompile()
# Check the quantized accuracy every N epochs
# Note: If you wish to just evaluate the QAT model (not the quantized model),
# then you can just call `torch.ao.quantization.move_exported_model_to_eval/train`.
# However, the latter API is not ready yet and will be available in the near future.
if (nepoch + 1) % num_epochs_between_evals == 0:
prepared_model_copy = copy.deepcopy(prepared_model)
quantized_model = convert_pt2e(prepared_model_copy)
top1, top5 = evaluate(quantized_model, criterion, data_loader_test, neval_batches=num_eval_batches)
print('Epoch %d: Evaluation accuracy on %d images, %2.2f' % (nepoch, num_eval_batches * eval_batch_size, top1.avg))
儲存與載入模型檢查點¶
PyTorch 2 Export QAT 流程的模型檢查點與任何其他訓練流程中的模型檢查點相同。它們可用於暫停訓練並在稍後恢復、從失敗的訓練執行中恢復,以及稍後在不同的機器上執行推論。您可以在訓練期間或訓練後儲存模型檢查點,如下所示:
checkpoint_path = "/path/to/my/checkpoint_%s.pth" % nepoch
torch.save(prepared_model.state_dict(), "checkpoint_path")
要載入檢查點,您必須以最初匯出和準備模型的完全相同方式匯出和準備模型。例如:
from torch._export import capture_pre_autograd_graph
from torch.ao.quantization.quantizer.xnnpack_quantizer import (
XNNPACKQuantizer,
get_symmetric_quantization_config,
)
from torchvision.models.resnet import resnet18
example_inputs = (torch.rand(2, 3, 224, 224),)
float_model = resnet18(pretrained=False)
exported_model = capture_pre_autograd_graph(float_model, example_inputs)
quantizer = XNNPACKQuantizer()
quantizer.set_global(get_symmetric_quantization_config(is_qat=True))
prepared_model = prepare_qat_pt2e(exported_model, quantizer)
prepared_model.load_state_dict(torch.load(checkpoint_path))
# resume training or perform inference
將已訓練的模型轉換為量化模型¶
convert_pt2e
接收經過校準的模型並產生量化模型。請注意,在推論之前,您必須首先呼叫 torch.ao.quantization.move_exported_model_to_eval()
,以確保像 dropout 這樣的操作在 eval 圖中表現正確。 否則,我們將在推論期間繼續在正向傳遞中錯誤地應用 dropout,例如。
quantized_model = convert_pt2e(prepared_model)
# move certain ops like dropout to eval mode, equivalent to `m.eval()`
torch.ao.quantization.move_exported_model_to_eval(m)
print(quantized_model)
top1, top5 = evaluate(quantized_model, criterion, data_loader_test, neval_batches=num_eval_batches)
print('Final evaluation accuracy on %d images, %2.2f' % (num_eval_batches * eval_batch_size, top1.avg))