国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

深度學(xué)習(xí)框架量化感知訓(xùn)練的思考及OneFlow的一種解決方案

共 28130字,需瀏覽 57分鐘

 ·

2021-10-03 05:47

【GiantPandaCV導(dǎo)語】這篇文章分享的是筆者最近在OneFlow做的一個(gè)項(xiàng)目,將Pytorch FX移植到OneFlow之后實(shí)現(xiàn)了自動(dòng)量化感知訓(xùn)練動(dòng)態(tài)圖模型(在Pytorch和OneFlow中都稱為nn.Module)?,F(xiàn)在用戶可以在自己構(gòu)建的nn.Module基礎(chǔ)上,修改很少的代碼即可完成從nn.Module量化感知訓(xùn)練到用TensorRT將量化感知訓(xùn)練后的模型部署到GPU上運(yùn)行的完整鏈路。在TensorRT上推理是利用了ONNX作為中間表示,即Oneflow動(dòng)態(tài)圖模型(nn.Module)->OneFlow量化感知訓(xùn)練模型(nn.Module)->OneFlow靜態(tài)圖(nn.Graph)->ONNX->TensorRT。量化感知訓(xùn)練是基于支持在Eager下寫Pass的FX模塊(FX被Pytorch率先提出,筆者將其基礎(chǔ)設(shè)施移植到了OneFlow)來完成的。讀者如果想體驗(yàn)這個(gè)功能可以按照本文的方法進(jìn)行操作,有任何使用上的問題可以聯(lián)系筆者。

0x0. 總覽

好久不見,大家國慶快樂!

相信不少小伙伴都了解或者使用了一些深度學(xué)習(xí)框架比如Pytorch,TensorFlow,OneFlow(也是筆者目前正在參與開發(fā)的)。但當(dāng)大家使用深度學(xué)習(xí)框架的訓(xùn)練量化方案時(shí)如果第一感覺就是太復(fù)雜了,那么你可能會(huì)對這篇文章感興趣!因?yàn)槲以?個(gè)月前開始接觸這個(gè)項(xiàng)目前,對量化感知訓(xùn)練的知識(shí)積累也非常少,并且我也會(huì)認(rèn)為各個(gè)框架的量化感知訓(xùn)練方案很復(fù)雜,甚至不想研究這些API。

這篇文章我會(huì)以Pytorch的兩代量化方案開始切入談一談他們的好處和壞處,然后我會(huì)講講我在吸收了Pytorch的部分優(yōu)秀成果(FX模塊)并加上一些自己的想法后把OneFlow的量化感知訓(xùn)練方案做成了什么樣子。這里先羅列一下這篇文章中涉及到的知識(shí)點(diǎn):

  • Pytorch FX模塊
  • Eager Pass
  • 量化感知訓(xùn)練
  • Conv+BN的融合
  • OneFlow的動(dòng)靜轉(zhuǎn)換(nn.Graph)
  • ONNX
  • TensorRT

如果你對上面的任意一個(gè)知識(shí)點(diǎn)不熟悉,那也是完全沒有關(guān)系的。實(shí)際上即使你只會(huì)用Pytorch搭建模型也可以快速把本文的量化感知訓(xùn)練方案用起來。因?yàn)榱炕兄?xùn)練的工作和模型轉(zhuǎn)化成ONNX以及用TensorRT來部署運(yùn)行的代碼我們在OneFlow社區(qū)中均開源了。

簡單總結(jié)一下就是,用戶可以基于OneFlow搭建一個(gè)動(dòng)態(tài)圖模型(即nn.Module,算子的API和Pytorch基本一樣),然后調(diào)用下面的幾行代碼就可以完成這個(gè)動(dòng)態(tài)圖模型(是一個(gè)nn.Module)自動(dòng)在合適的位置插入量化模塊生成一個(gè)量化模型(仍然是nn.Module),然后基于這個(gè)量化模型完成量化感知訓(xùn)練。

gm:?flow.fx.GraphModule?=?flow.fx.symbolic_trace(net)
qconfig?=?{
????'quantization_bit':?8,?
????'quantization_scheme':?"symmetric",?
????'quantization_formula':?"cambricon",?
????'per_layer_quantization':?True,
????'momentum':?0.95,
}

net?=?quantization_aware_training(gm,?flow.randn(1,?3,?32,?32),?qconfig)
net?=?net.to(device)

在訓(xùn)練完成后,調(diào)用下面的代碼完成訓(xùn)練量化模型到ONNX的轉(zhuǎn)換,并使用TensorRT在GPU上推理。

quantization_resnet18?=?quantization_aware_training(gm,?flow.randn(1,?3,?32,?32).to("cuda"),?qconfig)
quantization_resnet18?=?quantization_resnet18.to("cuda")
quantization_resnet18.eval()
checkpoint?=?flow.load('/home/zhangxiaoyu/oneflow-cifar/checkpoint/epoch_11_val_acc_83.280000')
quantization_resnet18.load_state_dict(checkpoint)

origin_gm:?flow.fx.GraphModule?=?flow.fx.symbolic_trace(resnet18)
dequantization_resnet18?=?dequantization_aware_training(origin_gm,?gm,?flow.randn(1,?3,?32,?32).to("cuda"),?qconfig)
dequantization_resnet18?=?dequantization_resnet18.to("cuda")
dequantization_resnet18.eval()

class?ResNet18Graph(flow.nn.Graph):
????def?__init__(self):
????????super().__init__()
????????self.m?=?dequantization_resnet18

????def?build(self,?x):
????????out?=?self.m(x)
????????return?out

def?test_resnet():???
????resnet_graph?=?ResNet18Graph()
????resnet_graph._compile(flow.randn(1,?3,?32,?32).to("cuda"))
????with?tempfile.TemporaryDirectory()?as?tmpdirname:
????????flow.save(dequantization_resnet18.state_dict(),?tmpdirname)
????????convert_to_onnx_and_check(resnet_graph,?flow_weight_dir=tmpdirname,?onnx_model_path="/tmp",?print_outlier=True)
????????ipt_dict,?onnx_res?=?run_onnx("/tmp/model.onnx",?get_onnx_provider("cpu"))
????????trt_res?=?run_tensorrt("/tmp/model.onnx",?ipt_dict[list(ipt_dict.keys())[0]])
????????compare_result(onnx_res,?trt_res,?atol=1e-4,?print_outlier=True)

test_resnet()

用戶只需要使用上面示例中的短短幾十行代碼就可以完成一個(gè)端到端的量化感知訓(xùn)練到GPU部署的全流程。所以我認(rèn)為這項(xiàng)工作是有趣并且相對簡潔的,當(dāng)然我更希望聽到用戶的想法,然后就寫了這篇文章來分享這個(gè)項(xiàng)目。這個(gè)項(xiàng)目的所有代碼均開源在了OneFlow社區(qū),下面是對應(yīng)的鏈接。如果你使用這個(gè)方案碰到了什么問題都可以第一時(shí)間聯(lián)系我。我的個(gè)人微信號(hào)是bbuf23333,來時(shí)請備注 量化感知訓(xùn)練

  • OneFlow FX(用來實(shí)現(xiàn)量化感知訓(xùn)練的基礎(chǔ)設(shè)施):https://github.com/Oneflow-Inc/oneflow/pull/5939
  • OneFlow Cifar(基于OneFlow FX量化訓(xùn)練Cifar10):https://github.com/BBuf/oneflow-cifar
  • OneFlow->ONNX和TensorRT運(yùn)行:https://github.com/Oneflow-Inc/oneflow_convert/pull/45

0x1. Pytorch量化方案的沉浮

這一節(jié)主要基于Pytorch的官方文檔:https://pytorch.org/docs/1.9.0/quantization.html來進(jìn)行說明。Pytorch第一代量化方案叫作Eager Mode Quantization,然后從1.8開始推出FX Graph Mode Quantization。Eager Mode Quantization需要用戶手動(dòng)更改模型,并手動(dòng)指定需要融合的Op。FX Graph Mode Quantization解放了用戶,一鍵自動(dòng)量化,無需用戶修改模型和關(guān)心內(nèi)部操作。這個(gè)改動(dòng)具體可以體現(xiàn)在下面的圖中。

Pytorch兩代量化方案的區(qū)別

下面分別解釋一下Pytorch這兩種量化方式的區(qū)別。

Eager Mode Quantization

class?Net(nn.Module):

????def?__init__(self,?num_channels=1):
????????super(Net,?self).__init__()
????????self.conv1?=?nn.Conv2d(num_channels,?40,?3,?1)
????????self.conv2?=?nn.Conv2d(40,?40,?3,?1)
????????self.fc?=?nn.Linear(5*5*40,?10)

????def?forward(self,?x):
????????x?=?F.relu(self.conv1(x))
????????x?=?F.max_pool2d(x,?2,?2)
????????x?=?F.relu(self.conv2(x))
????????x?=?F.max_pool2d(x,?2,?2)
????????x?=?x.reshape(-1,?5*5*40)
????????x?=?self.fc(x)
????????return?x

Pytorch可以在nn.Module的foward里面隨意構(gòu)造網(wǎng)絡(luò),可以調(diào)用其它nn.Module,也可以調(diào)用nn.functional.xxx,甚至可以在里面寫If這種控制邏輯。但這也帶來了一個(gè)問題,就是在Eager層面比較難獲取這個(gè)模型的圖結(jié)構(gòu)。所以在Eager Mode Quantization中,要量化這個(gè)網(wǎng)絡(luò)必須做手動(dòng)修改:

class?NetQuant(nn.Module):

????def?__init__(self,?num_channels=1):
????????super(NetQuant,?self).__init__()
????????self.conv1?=?nn.Conv2d(num_channels,?40,?3,?1)
????????self.relu1?=?nn.ReLU()
????????self.pool1?=?nn.MaxPool2d(2,?2)
????????self.conv2?=?nn.Conv2d(40,?40,?3,?1)
????????self.relu2?=?nn.ReLU()
????????self.pool2?=?nn.MaxPool2d(2,?2)
????????self.fc?=?nn.Linear(5*5*40,?10)

????????self.quant?=?torch.quantization.QuantStub()
????????self.dequant?=?torch.quantization.DeQuantStub()

????def?forward(self,?x):
????????x?=?self.quant(x)
????????x?=?self.relu1(self.conv1(x))
????????x?=?self.pool1(x)
????????x?=?self.relu2(self.conv2(x))
????????x?=?self.pool2(x)
????????x?=?x.reshape(-1,?5*5*40)
????????x?=?self.fc(x)
????????x?=?self.dequant(x)
????????return?x

也就是說,除了Conv,Linear這些含有參數(shù)的Module外,ReLU,MaxPool2d也要在__init__中定義,Eager Mode Quantization才可以正確處理。

除了這一點(diǎn),還有一些情況是需要Fuse之后做量化比如Conv+ReLU,那么還需要手動(dòng)指定這些層進(jìn)行折疊,目前這種量化模式支持ConV + BN、ConV + BN + ReLU、Conv + ReLU、Linear + ReLU、BN + ReLU的折疊。

model?=?NetQuant()model.qconfig?=?torch.quantization.get_default_qconfig('fbgemm')
modules_to_fuse?=?[['conv1',?'relu1'],?['conv2',?'relu2']]??#?指定合并layer的名字
model_fused?=?torch.quantization.fuse_modules(model,?modules_to_fuse)
model_prepared?=?torch.quantization.prepare(model_fused)
post_training_quantize(model_prepared,?train_loader)???#?這一步是做后訓(xùn)練量化
model_int8?=?torch.quantization.convert(model_prepared)

整個(gè)流程比較逆天,不知道有沒有人用。不過公眾號(hào)有小伙伴確實(shí)用過,見文章:Pytorch量化感知訓(xùn)練詳解

FX Graph Mode Quantization

關(guān)于Pytorch FX模塊是什么,我們放到下一節(jié)來講。

由于 Pytorch FX 可以自動(dòng)跟蹤 forward 里面的代碼,因此它是真正記錄了網(wǎng)絡(luò)里面的每個(gè)節(jié)點(diǎn),在 fuse 和動(dòng)態(tài)插入量化節(jié)點(diǎn)方面,比 Eager 模式強(qiáng)太多。對于前面那個(gè)模型代碼,我們不需要對網(wǎng)絡(luò)做修改,直接讓 FX 幫我們自動(dòng)修改網(wǎng)絡(luò)即可,一個(gè)使用示例如下:

from?torch.quantization?import?get_default_qconfig,?quantize_jit
from?torch.quantization.quantize_fx?import?prepare_fx,?convert_fx
model?=?Net()??
qconfig?=?get_default_qconfig("fbgemm")
qconfig_dict?=?{"":?qconfig}
model_prepared?=?prepare_fx(model,?qconfig_dict)
post_training_quantize(model_prepared,?train_loader)??????#?這一步是做后訓(xùn)練量化
model_int8?=?convert_fx(model_prepared)

基于這兩套量化方案來看,基于FX的量化方案顯然更加優(yōu)秀,因?yàn)樗恍枰脩粼诙x模型的時(shí)候做什么額外限制,用戶仍然是隨心所欲的寫模型代碼就行了,這才符合人的常識(shí)。我在做OneFlow的量化感知訓(xùn)練方案時(shí)也正是基于FX這個(gè)基礎(chǔ)設(shè)施(我將其核心功能移植到了OneFlow框架下,代碼鏈接第一節(jié)給了)來完成的。

另外在TensorRT的工程中:https://github.com/NVIDIA/TensorRT/tree/master/tools/pytorch-quantization發(fā)現(xiàn)Pytorch量化模型要轉(zhuǎn)為ONNX來部署現(xiàn)在似乎還是得基于第一個(gè)版本的方案,Pytorch FX這邊似乎想直接從nn.Module轉(zhuǎn)到TensorRT,不經(jīng)過ONNX的中間表示,所以我這里的技術(shù)路線還是有點(diǎn)不一樣。

0x2. OneFlow FX (在Eager中寫Pass)

FX可以用來做什么?

FX示意圖

FX可以將一個(gè)nn.Module變換后生成另外一個(gè)nn.Module,只需要在這個(gè)架構(gòu)的基礎(chǔ)上實(shí)現(xiàn)一些Transformation(也可以叫Pass),比如在Conv后自動(dòng)插入偽量化節(jié)點(diǎn)實(shí)現(xiàn)訓(xùn)練量化,然后生成GraphModule(這個(gè)也是nn.Module)進(jìn)行訓(xùn)練和轉(zhuǎn)為ONNX進(jìn)行部署。

OneFlow FX模塊在這個(gè)PR(https://github.com/Oneflow-Inc/oneflow/pull/5939)中實(shí)現(xiàn),這里復(fù)用了Pytorch FX基礎(chǔ)設(shè)施的核心邏輯和代碼,這個(gè)PR里的主要工作為:

  • [x] 精簡Pytorch FX的特殊設(shè)計(jì)比如對_C的Trace,和Jit的交互。保留核心功能,即Symbolic Tracing,Intermediate Representation和Transformation以及Python Codegen這4個(gè)組成部分。
  • [x] ?分步實(shí)現(xiàn)以上四大功能的代碼,完全適配OneFlow的相關(guān)設(shè)計(jì),現(xiàn)在可以一鍵import oneflow.fx來體驗(yàn)??梢訲race住基本所有OneFlow API搭建的Eager模型的結(jié)構(gòu),并將其變換成一個(gè)等價(jià)的nn.Module,我們還可以在這個(gè)nn.Module的基礎(chǔ)上自定義自己的Transformation Pass,我這里實(shí)現(xiàn)了Shape Infer和Quantization以及Dequantization的Pass。
  • [x] 增加AlexNet,ResNet50,MobileNetV2等模型的測試。

然后分享一下OneFlow FX的整體思路。

先看一個(gè)示例:

????import?oneflow
????#?Simple?module?for?demonstration
????class?MyModule(oneflow.nn.Module):
????????def?__init__(self):
????????????super().__init__()
????????????self.param?=?oneflow.nn.Parameter(oneflow.rand(3,?4))
????????????self.linear?=?oneflow.nn.Linear(4,?5)
????????def?forward(self,?x):
????????????return?self.linear(x?+?self.param).clamp(min=0.0,?max=1.0)
????module?=?MyModule()
????from?oneflow.fx?import?symbolic_trace
????#?Symbolic?tracing?frontend?-?captures?the?semantics?of?the?module
????symbolic_traced?:?oneflow.fx.GraphModule?=?symbolic_trace(module)
????#?High-level?intermediate?representation?(IR)?-?Graph?representation
????print(symbolic_traced.graph)
????"""
????graph():
????????%x?:?[#users=1]?=?placeholder[target=x]
????????%param?:?[#users=1]?=?get_attr[target=param]
????????%add?:?[#users=1]?=?call_function[target=operator.add](args?=?(%x,?%param),?kwargs?=?{})
????????%linear?:?[#users=1]?=?call_module[target=linear](args?=?(%add,),?kwargs?=?{})
????????%clamp?:?[#users=1]?=?call_method[target=clamp](args?=?(%linear,),?kwargs?=?{min:?0.0,?max:?1.0})
????????return?clamp

????"""

????#?Code?generation?-?valid?Python?code
????print(symbolic_traced.code)
????"""
????def?forward(self,?x):
????????param?=?self.param
????????add?=?x?+?param;??x?=?param?=?None
????????linear?=?self.linear(add);??add?=?None
????????clamp?=?linear.clamp(min?=?0.0,?max?=?1.0);??linear?=?None
????????return?clamp
????"""

在FX中有一個(gè)Proxy類,它會(huì)把oneflow中所有的call_methodcall_function以及math庫中的函數(shù)和常見的魔法函數(shù)都包裝一遍來記錄OneFlow中所有的運(yùn)算符,這個(gè)在import oneflow.fx時(shí)就做好了。然后在傳入一個(gè)nn.Module調(diào)用symbolic_trace進(jìn)行跟蹤代碼的時(shí)候會(huì)首先處理__init__中的其它nn.Module,把這些nn.Module也用Proxy包起來,同時(shí)輸入數(shù)據(jù)也要包起來。

用Proxy包好所有程序中可能存在的運(yùn)算符之后就執(zhí)行一遍forward,這個(gè)forward的輸入數(shù)據(jù)不再是Tensor而是Proxy(Tensor)。由于程序的執(zhí)行過程類似于一個(gè)運(yùn)算符和數(shù)據(jù)入棧出棧的過程,所以我們可以直接按照這個(gè)執(zhí)行順序?qū)偛庞肞roxy記錄下來的數(shù)據(jù)和Op進(jìn)行unpack,unpack之后可以拿到真實(shí)的Tensor, Parameter和運(yùn)算符等等,我們將這些數(shù)據(jù)和運(yùn)算符當(dāng)作點(diǎn)和邊去構(gòu)造一個(gè)新的Graph。那么Graph是怎么轉(zhuǎn)化成nn.Module的呢?FX中通過引入GraphModule的數(shù)據(jù)結(jié)構(gòu)來持有這個(gè)Graph,此外GraphModule還持有codefoward成員,這兩者都是基于Graph自動(dòng)生成的,注意GraphModule仍然是nn.Module。

自動(dòng)生成的代碼就是GraphModule中的code,打印出來其實(shí)就是整個(gè)forward函數(shù)的完整執(zhí)行過程。

另外FX還提供了一個(gè)Interpreter類用來讓用戶自定義nn.Module的執(zhí)行過程,比如這個(gè)PR提供了一個(gè)基于這個(gè)類做所有中間Tensor形狀推導(dǎo)的Pass。另外還提供了一個(gè)基于pydotGraphModule結(jié)構(gòu)可視化的Pass,如下圖。

基于Pydot可視化動(dòng)態(tài)圖模式搭建的模型

相信到這里大家對FX有一個(gè)了解了,這里最棒的一個(gè)功能就是我們可以對nn.Module進(jìn)行修改,然后返回變化后的nn.Module。說

到這里,我們自然能想到量化感知訓(xùn)練不就是把Conv+BN或者Conv,Linear等組件替換為插入了偽量化節(jié)點(diǎn)的組件嗎?所以我們基于FX來寫一個(gè)Pass就可以完成這件事了。

這就是上面說的,FX支持在Eager寫Pass。

然而FX也存在缺陷,目前無法處理控制流,需要注意網(wǎng)絡(luò)中不要帶控制流(不過這一點(diǎn)暫時(shí)影響不大,因?yàn)橛脩粢话愣疾粫?huì)部署含有控制流的網(wǎng)絡(luò),如果有這個(gè)需求我們可以交流)。

0x3. 實(shí)現(xiàn)量化感知訓(xùn)練Pass

有了OneFlow FX之后我們就可以實(shí)現(xiàn)一個(gè)量化感知訓(xùn)練的Pass來將用戶自定義的網(wǎng)絡(luò)中自動(dòng)插入量化感知訓(xùn)練組件來完成量化感知訓(xùn)練了。

以ResNet18為例,它只有Conv+BN這種模式,即對于任意一個(gè)卷積層后面都跟了一個(gè)BN層,在推理的時(shí)候TensorRT會(huì)做Conv+BN的融合,那么我們在訓(xùn)練的時(shí)候也是必須要做Conv+BN的融合的,不然會(huì)影響部署的精度。所以,我們首先需要把BN層的參數(shù)和卷積層的參數(shù)融合,然后再對這個(gè)參數(shù)做量化,具體過程如下圖所示:

訓(xùn)練模擬量化fold bn過程

下面是Conv和BN融合的公式:

所以:

公式中的,分別表示卷積層的權(quán)值與偏置,分別為卷積層的輸入與輸出,則根據(jù)的計(jì)算公式,可以推出融合了batchnorm參數(shù)之后的權(quán)值與偏置,。

按照這個(gè)公式就可以實(shí)現(xiàn)Conv+BN融合后的量化感知訓(xùn)練組件,在實(shí)現(xiàn)中對訓(xùn)練和推理的處理有些不一樣的地方,我在代碼中標(biāo)注出來了。

class?QConvBN(flow.nn.Module):
????def?__init__(
????????self,
????????conv_module,
????????bn_module,
????????quantization_bit=8,
????????quantization_scheme="symmetric",
????????quantization_formula="google",
????????per_layer_quantization=True,
????????momentum=0.95,
????)
:

????????super().__init__()
????????self.quantization_bit?=?quantization_bit
????????self.quantization_scheme?=?quantization_scheme
????????self.quantization_formula?=?quantization_formula
????????self.per_layer_quantization?=?per_layer_quantization
????????self.conv_module?=?conv_module
????????self.bn_module?=?bn_module

????????self.moving_min_max_observer?=?flow.nn.MovingAverageMinMaxObserver(
????????????training=self.training,
????????????quantization_formula=quantization_formula,
????????????stop_update_after_iters=1,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????????momentum=momentum,
????????)

????????self.min_max_observer?=?flow.nn.MinMaxObserver(
????????????quantization_formula=quantization_formula,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????????per_layer_quantization=per_layer_quantization,
????????)

????????self.fake_quantization?=?flow.nn.FakeQuantization(
????????????quantization_formula=quantization_formula,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????)

????def?fold_bn(self,?mean,?std):
????????if?self.bn_module.affine:
????????????gamma_?=?self.bn_module.weight?/?std
????????????weight?=?self.conv_module.weight?*?gamma_.view(
????????????????self.conv_module.out_channels,?1,?1,?1
????????????)
????????????if?self.conv_module.bias?is?not?None:
????????????????bias?=?(
????????????????????gamma_?*?self.conv_module.bias?-?gamma_?*?mean?+?self.bn_module.bias
????????????????)
????????????else:
????????????????bias?=?self.bn_module.bias?-?gamma_?*?mean
????????else:
????????????gamma_?=?1?/?std
????????????weight?=?self.conv_module.weight?*?gamma_
????????????if?self.conv_module.bias?is?not?None:
????????????????bias?=?gamma_?*?self.conv_module.bias?-?gamma_?*?mean
????????????else:
????????????????bias?=?-gamma_?*?mean

????????return?weight,?bias

????def?forward(self,?x):
????????scale,?zero_point?=?self.moving_min_max_observer(
????????????x,?flow.tensor([0],?dtype=flow.int64).to(x.device.type)
????????)
????????x?=?self.fake_quantization(x,?scale,?zero_point)
????????if?self.training:
????????????y?=?flow.nn.functional.conv2d(
????????????????x,
????????????????self.conv_module.weight,
????????????????self.conv_module.bias,
????????????????stride=self.conv_module.stride,
????????????????padding=self.conv_module.padding,
????????????????dilation=self.conv_module.dilation,
????????????????groups=self.conv_module.groups,
????????????)
????????????y?=?y.permute(1,?0,?2,?3)??#?NCHW?->?CNHW
????????????y?=?y.view(self.conv_module.out_channels,?-1)??#?CNHW?->?C,NHW
????????????mean?=?y.mean(1)
????????????var?=?y.var(1)
????????????with?flow.no_grad():
????????????????self.bn_module.running_mean?=?(
????????????????????self.bn_module.momentum?*?self.bn_module.running_mean
????????????????????+?(1?-?self.bn_module.momentum)?*?mean
????????????????)
????????????????self.bn_module.running_var?=?(
????????????????????self.bn_module.momentum?*?self.bn_module.running_var
????????????????????+?(1?-?self.bn_module.momentum)?*?var
????????????????)
????????else:
????????????mean?=?flow.Tensor(self.bn_module.running_mean)
????????????var?=?flow.Tensor(self.bn_module.running_var)

????????std?=?flow.sqrt(var?+?self.bn_module.eps)
????????weight,?bias?=?self.fold_bn(mean,?std)

????????weight_scale,?weight_zero_point?=?self.min_max_observer(weight)
????????res?=?flow.nn.functional.conv2d(
????????????x,
????????????self.fake_quantization(weight,?weight_scale,?weight_zero_point),
????????????bias,
????????????stride=self.conv_module.stride,
????????????padding=self.conv_module.padding,
????????????dilation=self.conv_module.dilation,
????????????groups=self.conv_module.groups,
????????)
????????return?res

實(shí)現(xiàn)了這個(gè)組件之后我們就可以實(shí)現(xiàn)一個(gè)量化感知訓(xùn)練Pass,即將用戶的nn.Module抽象的計(jì)算圖中的Conv+BN都替換成這個(gè)QConvBN組件,替換部分的代碼實(shí)現(xiàn)如下:

for?x?in?gm.graph.nodes:
????????if?x.target?in?insert_place:
????????????with?gm.graph.inserting_after(x):
????????????????y?=?x.next
????????????????if?(
????????????????????isinstance(insert_op_state[x.target],?flow.nn.Conv2d)
????????????????????and?y.target?in?insert_place
????????????????????and?isinstance(insert_op_state[y.target],?flow.nn.BatchNorm2d)
????????????????):
????????????????????now_target?=?get_current_module_space(x.target)
????????????????????if?now_target?==?"":
????????????????????????now_target?=?f"fake_conv_bn.{cnt}"
????????????????????else:
????????????????????????now_target?=?(
????????????????????????????f"{get_current_module_space(x.target)}.fake_conv_bn.{cnt}"
????????????????????????)
????????????????????gm.add_submodule(
????????????????????????now_target,
????????????????????????QConvBN(
????????????????????????????insert_op_state[x.target],
????????????????????????????insert_op_state[y.target],
????????????????????????????quantization_bit,
????????????????????????????quantization_scheme,
????????????????????????????quantization_formula,
????????????????????????????per_layer_quantization,
????????????????????????????momentum,
????????????????????????),
????????????????????)
????????????????????y.replace_all_uses_with(x)
????????????????????gm.graph.erase_node(y)
????????????????????gm.delete_submodule(y.target)
????????????????????qconvbn?=?gm.graph.call_module(module_name=now_target,?args=x.args,)
????????????????????cnt?=?cnt?+?1
????????????????????x.replace_all_uses_with(qconvbn)
????????????????????gm.graph.erase_node(x)
????????????????????gm.delete_submodule(x.target)

gm(ResNet18 Trace出來的GraphModule,仍然是nn.Module)中找到Conv+BN的組件,將其刪除然后替換成QConvBN組件。

0x4. 基于ResNet18量化感知訓(xùn)練Cifar10

基于上面實(shí)現(xiàn)的量化Pass,我們就可以方便的對自定義的模型進(jìn)行量化感知訓(xùn)練了,以ResNet18為例,我們在原始的動(dòng)態(tài)圖訓(xùn)練代碼基礎(chǔ)上加上下面幾行代碼就可以了。

gm:?flow.fx.GraphModule?=?flow.fx.symbolic_trace(net)
qconfig?=?{
????'quantization_bit':?8,?
????'quantization_scheme':?"symmetric",?
????'quantization_formula':?"cambricon",?
????'per_layer_quantization':?True,
????'momentum':?0.95,
}

net?=?quantization_aware_training(gm,?flow.randn(1,?3,?32,?32),?qconfig)
net?=?net.to(device)

這里qconfig讓用戶可以方便的配置OneFlow支持的各種量化方式。具體可以看之前的文章介紹:基于OneFlow實(shí)現(xiàn)量化感知訓(xùn)練

第一個(gè)net就是用戶定義的動(dòng)態(tài)圖模型,經(jīng)過這個(gè)Pass之后獲得新的net,新的net就已經(jīng)自動(dòng)插入好了量化感知訓(xùn)練組件。其它的訓(xùn)練和測試的過程和普通的FP32訓(xùn)練完全一致,就不贅述了。我基于ResNet18在Cifar10上訓(xùn)練了幾個(gè)OneFlow支持的量化配置,均訓(xùn)練了200個(gè)Epoch,超參一致,結(jié)果如下:

Note:
The?`momentum`?parameter?in?the?`MovingAverageMinMaxObserver`?class?defaults?to?0.95,?which?will?not?be?changed?in?the?following?experiments.?
##?Accuracy
|?Model?????????????|?quantization_bit?|?quantization_scheme?|?quantization_formula?|?per_layer_quantization?|?Acc?|
|?-----------------?|?-----------?|?-----------?|?-----------?|?-----------?|?-----------?|
|?ResNet18??????????|??8?????|??symmetric??????|?google???????|???True?????|??95.19%??????|?
|?ResNet18??????????|??8?????|??symmetric??????|?google???????|???False????|??95.24%??????|?
|?ResNet18??????????|??8?????|??affine?????????|?google???????|???True?????|??95.32%??????|?
|?ResNet18??????????|??8?????|??affine?????????|?google???????|???False????|??95.30%??????|?
|?ResNet18??????????|??8?????|??symmetric??????|?cambricon????|???True?????|??95.19%??????|

工程地址:https://github.com/BBuf/oneflow-cifar。ResNet18在Cifar10上基于FP32訓(xùn)練的精度是:95.62%。這里各種量化參數(shù)下的量化感知訓(xùn)練精度均和原始精度持平。上面的cambricon代表的是寒武紀(jì)量化方案,google代表的是Google的量化方案。

0x5. 基于量化感知訓(xùn)練模型改寫原始模型

上面我們已經(jīng)基于量化感知訓(xùn)練模型進(jìn)行了量化感知訓(xùn)練,接下來我們要考慮怎么部署這個(gè)量化感知訓(xùn)練模型了。顯然現(xiàn)在這個(gè)模型不是我們期望部署的樣子,因?yàn)槲覀冇脕聿渴鸬哪P虰N應(yīng)該已經(jīng)合并到卷積層里了,而不是被保留下來。所以我們要基于量化感知訓(xùn)練模型的參數(shù)對原始模型進(jìn)行改寫,然后將其用于轉(zhuǎn)化ONNX再到TensorRT。

這里和量化感知訓(xùn)練類似,我們實(shí)現(xiàn)一個(gè)dequantization Pass。這個(gè)Pass用來將QConvBN組件替換成一個(gè)DConv2d組件。DConv2d組件代碼實(shí)現(xiàn)如下:

class?DConv2d(flow.nn.Conv2d):
????def?__init__(
????????self,
????????in_channels,
????????out_channels,
????????kernel_size,
????????stride,
????????padding,
????????dilation,
????????groups,
????????quantization_bit=8,
????????quantization_scheme="symmetric",
????????quantization_formula="google",
????????per_layer_quantization=True,
????????momentum=0.95,
????)
?->?None:

????????super(DConv2d,?self).__init__(
????????????in_channels,?out_channels,?kernel_size,?stride,?padding,?dilation,?groups
????????)

????????self.moving_min_max_observer?=?flow.nn.MovingAverageMinMaxObserver(
????????????training=self.training,
????????????quantization_formula=quantization_formula,
????????????stop_update_after_iters=1,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????????momentum=momentum,
????????)

????????self.min_max_observer?=?flow.nn.MinMaxObserver(
????????????quantization_formula=quantization_formula,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????????per_layer_quantization=per_layer_quantization,
????????)

????????self.fake_quantization?=?flow.nn.FakeQuantization(
????????????quantization_formula=quantization_formula,
????????????quantization_bit=quantization_bit,
????????????quantization_scheme=quantization_scheme,
????????)

????????self.register_buffer("new_zero",?flow.Tensor(1))
????????self.new_zero.fill_(0)

????def?forward(self,?x):
????????scale,?zero_point?=?self.moving_min_max_observer(
????????????x,?self.new_zero.to(flow.int64).to(x.device.type)
????????)
????????x?=?self.fake_quantization(x,?scale,?zero_point)
????????return?flow.nn.functional.conv2d(
????????????x,
????????????self.weight,
????????????self.bias,
????????????stride=self.stride,
????????????padding=self.padding,
????????????dilation=self.dilation,
????????????groups=self.groups,
????????)

然后我們只需要將原始的ResNet18模型里面的Conv+BN換成這個(gè)組件即可,請注意!!!這個(gè)組件的權(quán)重和偏置以及moving_min_max_observermoving_min/max參數(shù)要賦值為訓(xùn)練好的量化感知模型的QConvBN組件對應(yīng)的權(quán)重和偏置以及moving_min_max_observermoving_min/max參數(shù)。dequantization Pass的核心部分如下:

for?x?in?origin_gm.graph.nodes:
????????if?x.target?in?insert_place:
????????????with?origin_gm.graph.inserting_after(x):
????????????????y?=?x.next
????????????????if?(
????????????????????isinstance(insert_op_state[x.target],?flow.nn.Conv2d)
????????????????????and?y.target?in?insert_place
????????????????????and?isinstance(insert_op_state[y.target],?flow.nn.BatchNorm2d)
????????????????):
????????????????????now_target?=?get_current_module_space(x.target)
????????????????????if?now_target?==?"":
????????????????????????now_target?=?f"fake_conv_bn.{cnt}"
????????????????????else:
????????????????????????now_target?=?(
????????????????????????????f"{get_current_module_space(x.target)}.fake_conv_bn.{cnt}"
????????????????????????)

????????????????????dequanzation_conv?=?DConv2d(
????????????????????????quantization_op_state[now_target].conv_module.in_channels,?
????????????????????????quantization_op_state[now_target].conv_module.out_channels,
????????????????????????quantization_op_state[now_target].conv_module.kernel_size,
????????????????????????quantization_op_state[now_target].conv_module.stride,
????????????????????????quantization_op_state[now_target].conv_module.padding,
????????????????????????quantization_op_state[now_target].conv_module.dilation,
????????????????????????quantization_op_state[now_target].conv_module.groups,
????????????????????????quantization_bit,
????????????????????????quantization_scheme,
????????????????????????quantization_formula,
????????????????????????per_layer_quantization,
????????????????????????momentum,
????????????????????????)
????????????????????mean?=?flow.Tensor(quantization_op_state[now_target].bn_module.running_mean)
????????????????????var?=?flow.Tensor(quantization_op_state[now_target].bn_module.running_var)
????????????????????std?=?flow.sqrt(var?+?quantization_op_state[now_target].bn_module.eps)

????????????????????if?quantization_op_state[now_target].bn_module.affine:
????????????????????????gamma_?=?quantization_op_state[now_target].bn_module.weight?/?std
????????????????????????weight?=?quantization_op_state[now_target].conv_module.weight?*?gamma_.view(
????????????????????????????quantization_op_state[now_target].conv_module.out_channels,?1,?1,?1
????????????????????????)
????????????????????????if?quantization_op_state[now_target].conv_module.bias?is?not?None:
????????????????????????????bias?=?(
????????????????????????????????gamma_?*?quantization_op_state[now_target].conv_module.bias?-?gamma_?*?mean?+?quantization_op_state[now_target].bn_module.bias
????????????????????????????)
????????????????????????else:
????????????????????????????bias?=?quantization_op_state[now_target].bn_module.bias?-?gamma_?*?mean
????????????????????else:
????????????????????????gamma_?=?1?/?std
????????????????????????weight?=?quantization_op_state[now_target].conv_module.weight?*?gamma_
????????????????????????if?quantization_op_state[now_target].conv_module.bias?is?not?None:
????????????????????????????bias?=?gamma_?*?quantization_op_state[now_target].conv_module.bias?-?gamma_?*?mean
????????????????????????else:
????????????????????????????bias?=?-gamma_?*?mean

????????????????????dequanzation_conv.weight?=?flow.nn.Parameter(weight)
????????????????????dequanzation_conv.bias?=?flow.nn.Parameter(bias)
????????????????????dequanzation_conv.moving_min_max_observer.moving_max?=?quantization_op_state[now_target].moving_min_max_observer.moving_max
????????????????????dequanzation_conv.moving_min_max_observer.moving_min?=?quantization_op_state[now_target].moving_min_max_observer.moving_min

????????????????????origin_gm.add_submodule(
????????????????????????now_target,
????????????????????????dequanzation_conv,
????????????????????)
????????????????????y.replace_all_uses_with(x)
????????????????????origin_gm.graph.erase_node(y)
????????????????????origin_gm.delete_submodule(y.target)
????????????????????qconvbn?=?origin_gm.graph.call_module(module_name=now_target,?args=x.args,)
????????????????????cnt?=?cnt?+?1
????????????????????x.replace_all_uses_with(qconvbn)
????????????????????origin_gm.graph.erase_node(x)
????????????????????origin_gm.delete_submodule(x.target)

這里手動(dòng)執(zhí)行了Conv和BN融合的工作并把融合后的權(quán)重和偏置賦給DConv2d組件。

0x6. 轉(zhuǎn)換ONNX以及TensorRT推理

基于量化感知訓(xùn)練的模型以及dequantization Pass,我們就可以獲得用于推理時(shí)的nn.Module了。我們將這個(gè)nn.Module轉(zhuǎn)換成ONNX然后再放到TensorRT中進(jìn)行推理就可以了。這部分的示例代碼在:https://github.com/Oneflow-Inc/oneflow_convert/blob/add_fx_train_quantization/examples/oneflow2onnx/quantization/test_resnet18.py。我們截取核心部分進(jìn)行解釋。

#?加載訓(xùn)練好的量化模型權(quán)重
quantization_resnet18?=?quantization_aware_training(gm,?flow.randn(1,?3,?32,?32).to("cuda"),?qconfig)
quantization_resnet18?=?quantization_resnet18.to("cuda")
quantization_resnet18.eval()
checkpoint?=?flow.load('/home/zhangxiaoyu/oneflow-cifar/checkpoint/epoch_11_val_acc_83.280000')
quantization_resnet18.load_state_dict(checkpoint)

#?基于量化感知訓(xùn)練模型改寫原始模型
origin_gm:?flow.fx.GraphModule?=?flow.fx.symbolic_trace(resnet18)
dequantization_resnet18?=?dequantization_aware_training(origin_gm,?gm,?flow.randn(1,?3,?32,?32).to("cuda"),?qconfig)
dequantization_resnet18?=?dequantization_resnet18.to("cuda")
dequantization_resnet18.eval()

#?nn.Graph是轉(zhuǎn)ONNX的橋梁,是把OneFlow的動(dòng)態(tài)圖轉(zhuǎn)為靜態(tài)圖
class?ResNet18Graph(flow.nn.Graph):
????def?__init__(self):
????????super().__init__()
????????self.m?=?dequantization_resnet18

????def?build(self,?x):
????????out?=?self.m(x)
????????return?out
#?測試函數(shù)
def?test_resnet():???
????resnet_graph?=?ResNet18Graph()
????resnet_graph._compile(flow.randn(1,?3,?32,?32).to("cuda"))
????with?tempfile.TemporaryDirectory()?as?tmpdirname:
????????flow.save(dequantization_resnet18.state_dict(),?tmpdirname)
????????convert_to_onnx_and_check(resnet_graph,?flow_weight_dir=tmpdirname,?onnx_model_path="/tmp",?print_outlier=True)
????????ipt_dict,?onnx_res?=?run_onnx("/tmp/model.onnx",?get_onnx_provider("cpu"))
????????trt_res?=?run_tensorrt("/tmp/model.onnx",?ipt_dict[list(ipt_dict.keys())[0]])
????????compare_result(onnx_res,?trt_res,?atol=1e-4,?print_outlier=True)

test_resnet()

首先我們使用dequantization Pass將原始模型改寫成了部署時(shí)的模型,并且在這個(gè)Pass中同步處理好了權(quán)重的更改。然后我們將現(xiàn)在需要部署的這個(gè)模型(類型是nn.Module)通過OneFlow的nn.Graph將其轉(zhuǎn)為靜態(tài)圖,nn.Graph的資料見:https://docs.oneflow.org/master/basics/08_nn_graph.html。

為什么要nn.Graph這一步?這是因?yàn)镺neFlow的轉(zhuǎn)化ONNX工具是基于靜態(tài)圖做的,所以額外多了這一步,如果你不想理解也沒關(guān)系,上面的代碼中已經(jīng)展示了完整的用法了。

要使用OneFlow->ONNX的轉(zhuǎn)化工具需要安裝下面的包:

python>=3.5
onnx>=1.8.0
onnxruntime>=1.6.0
oneflow>=0.5.0

然后pip install oneflow_onnx

然后調(diào)用oneflow_onnx中的convert_to_onnx_and_check API將量化訓(xùn)練模型轉(zhuǎn)化為ONNX。我們看一眼量化感知訓(xùn)練后的ResNet18轉(zhuǎn)化成ONNX之后長什么樣子吧。

ResNet18量化感知訓(xùn)練模型

然后我們還需要用TesnsorRT來運(yùn)行這個(gè)量化感知訓(xùn)練模型,也要配置一些環(huán)境。我們需要安裝:

onnx>=1.8.0
onnxruntime-gpu>=1.8.0
opencv-python
pytest
nvidia-tensorrt==8.0.0.3
pycuda
flake8

這些包就緒之后就可以使用TensorRT來推理了。即上面的代碼:

ipt_dict,?onnx_res?=?run_onnx("/tmp/model.onnx",?get_onnx_provider("cpu"))
trt_res?=?run_tensorrt("/tmp/model.onnx",?ipt_dict[list(ipt_dict.keys())[0]])
compare_result(onnx_res,?trt_res,?atol=1e-4,?print_outlier=True)

具體的推理代碼和其它細(xì)節(jié)可以去代碼倉庫看,這里展示一下最后的結(jié)果。在相同的輸隨機(jī)入下,ONNX的結(jié)果和TensorRT推理結(jié)果基本一致:

-2.9825006?-2.9825
-5.438802?-5.4388037
3.5198674?3.5198674
2.409646?2.4096458
4.5826764?4.5826764
0.019911028?0.019910894
6.6347113?6.634712
-3.5996702?-3.5996711
-1.3407612?-1.340761
-3.8473191?-3.847319

至此,我們完成了將原始的動(dòng)態(tài)圖模型通過量化感知訓(xùn)練后部署到了GPU上進(jìn)行推理,整個(gè)過程雖然我開發(fā)的波折比較大,但總算完成了基礎(chǔ)功能的開發(fā),感謝我的同事們。

我想你可能會(huì)好奇,這里為什么沒有給精度和速度對比,因?yàn)槟壳拔沂稚峡ú粔蜻€不能做更好的實(shí)驗(yàn)(比如用更大的數(shù)據(jù)集訓(xùn)練)所以只能用Cifar10跑一下精度。關(guān)于速度測試方面,TensorRT那部分需要排除編譯engine的影響只計(jì)算推理那部分的時(shí)間,我還沒有改那部分代碼,讀者如果感興趣可以先自行計(jì)算一下時(shí)間。后面可能會(huì)專門寫一篇文章來介紹一下部署前后的精度和速度對比,另外目前實(shí)現(xiàn)的方案可能還存在漏洞需要更加精細(xì)的Check。

總的來說,這篇文章只是一篇學(xué)習(xí)交流筆記,所以目前并不會(huì)正式的給出量化感知訓(xùn)練精度和速度的BenchMark。因?yàn)樵诤喜⒌絆neFlow主分支前還有諸多的工程問題需要解決。

0x7. 總結(jié)

這篇文章分享的是筆者最近在OneFlow做的一個(gè)項(xiàng)目,將Pytorch FX移植到OneFlow之后實(shí)現(xiàn)了自動(dòng)量化感知訓(xùn)練動(dòng)態(tài)圖模型(在Pytorch和OneFlow中都稱為nn.Module)?,F(xiàn)在用戶可以在自己構(gòu)建的nn.Module基礎(chǔ)上,修改很少的代碼即可完成從nn.Module量化感知訓(xùn)練到用TensorRT將量化感知訓(xùn)練后的模型部署到GPU上運(yùn)行的完整鏈路。在TensorRT上推理是利用了ONNX作為中間表示,即Oneflow動(dòng)態(tài)圖模型(nn.Module)->OneFlow量化感知訓(xùn)練模型(nn.Module)->OneFlow靜態(tài)圖(nn.Graph)->ONNX->TensorRT。量化感知訓(xùn)練是基于支持在Eager下寫Pass的FX模塊(FX被Pytorch率先提出,筆者將其基礎(chǔ)設(shè)施移植到了OneFlow)來完成的。讀者如果想體驗(yàn)這個(gè)功能可以按照本文的方法進(jìn)行操作,有任何使用上的問題可以聯(lián)系筆者。

0x8. 相關(guān)鏈接和學(xué)習(xí)資料

瀏覽 103
點(diǎn)贊
評論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評論
圖片
表情
推薦
點(diǎn)贊
評論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 亚洲中文av| 成人一区二区三区四区五区| 欧美日韩国产在线观看| 国产精品a片| 欧美精品成人免费| 91精品视频在线免费观看| 一级黄色录相片| 欧美成人无码片免费看A片秀色 | 国产精品黄色片| 91人人妻人人爽| 婷婷五月999| 亚洲中文在线播放| 久99久视频| 在线操逼视频| 广西少妇BBwBBwBBw| 99视频在线免费观看| 天堂成人av| 亚洲综合在线视频| 日韩一级网站| AV网站免费看| 午夜理伦| 99久热在线精品| 在线免费观看国产视频| 国产清纯可爱美女自卫裸贷偷情| 在线黄色网| 一二区视频| 中文字幕人妻系列| 亚洲精品18禁| 人妻熟女88AⅤ| 日韩一级中文字幕| 视频一视频二在线视频| 51成人网站免费| 色欲av伊人久久大香线蕉影院| 狠狠操免费视频| 俺来也听听婷婷| 男人的天堂婷婷| 午夜午夜福利理论片在线播放 | 啪啪视频国产| 亚洲一级二级| 日本www色| 2017天天干| 精品国产欧美| 黄色三级电影| 蜜桃91在线观看| 操穴网| 熟女啪啪| 精品蜜桃一区内容| 成人在线免费观看视频| 日韩欧美在线视频观看| 丁香婷婷色| 欧美一级黄| 欧美日韩中文字幕在线视频| 欧美成人免费在线| 亚洲热在线视频| 成人性生交片无码免费看人| 国产毛片欧美毛片高潮| 91麻豆福利在线观看| 综合伊人| 天天爱夜夜操| 久草手机在线| 免费黄色一级片| 插菊花综合网3| 蜜桃视频在线入口www| 高清无码免费看| 韩国三级HD久久精品| 亚洲免费观看高清完| 天天视频亚洲| 国产精品视频久久久久| 日韩无码二级| 麻豆videos| 女神思瑞精品一区二区三区| 亚洲AV电影在线观看| 国产伦精品一区二区三区妓女| 一区二区三区高清无码| 国产乱国产乱300精品| 亚洲综合在线网| 中文字幕无码日韩| 嫩苞又嫩又紧AV无码| 操逼操逼逼| 国产一级片电影| 天天爽夜夜爽夜夜爽精品| 北京熟妇搡BBBB搡BBBB电影| 尻屄视频网站| 国产精品免费一区二区三区四区视频| 嫩BBB搡BBBB搡BBBB| 熟女啪啪| 黄色一级在线| 污视频在线观看免费| 四虎Av| 欧美18禁黄免费网站| 超碰青娱乐| 免费黄色电影在线观看| 最新一区二区| 日韩日逼视频| 在线播放你懂的| 老女人的逼| 91豆花视频| 日本视频精品| 一级黄色生活片| 国产精品丝袜| 青青草原视频在线| 97亚洲精品| 有码中文字幕在线观看| 国产精品99久久久久久成人| 中国黄色大片| 农村乱子伦毛片国产乱| 日韩黄色一级视频| 一区二区三区四区五区在线| 日本不卡三区| 国产美女自拍| 国产手机AV在线| 91免费在线视频观看| 国产精品扒开腿| 日韩中文字幕一区二区| 狠狠91| 久久久黄色电影| 91性爱视频在线观看| 欧美视频基地| 欧美性综合| 成人国产精品视频| 超碰p| 免费看无码一级A片放24小时| 一区精品| 国产美女精品久久AV爽| 韩国精品一区| 国产影视av| 中文字幕+乱码+中文乱码91在线观看 | 国产精品一区二区三区四区| 北条麻妃电影九九九| 精品看片| 国产在线你懂得| 日韩日韩日韩日韩日韩| 在线观看www视频| 日本一级大片| 中文字幕+乱码+中文乱码电影| 操屄影院| 97人人操| 色婷婷久久久久swag精品| 在线观看一区二区视频| 色香蕉在线视频| 亚洲无码av中文字幕| 3d动漫精品一区二区三区在线观看| 乱子伦国产精品www| 蜜桃视频一区二区三区四区使用方法| 影音先锋国产资源| 加勒比无码在线| 成人片成人网久久蜜桃臀| 人妻少妇被猛烈进入中文字幕| 熟妇精品| 久久久久久久网| 97人妻精品一区二区三区图片| 亚欧免费视频| 国产激情无码视频| 日韩欧美在线观看| 毛片网站大全| 欧美一区二区三区系列电影| 二区三区免费| 小黄片在线看| 黄色综合| 乳揉みま痴汉电车羽月希免费观看| 丰满人妻一区二区三区Av猛交| 中文字幕理论片| 大鸡巴久久久久久| 一区二区三区久久久久〖网:.〗| 一级黄色免费电影| 国产午夜福利视频| 欧美激情DVD| 国产成人三级片在线观看| 77777精品成人免费A片| 国产精品久久久久精| 爱爱黄色视频| 无码一级片| 成人国产精品在线观看| 欧美日韩中文视频| 99精品免费观看| 日操夜操| 99在线视频免费观看| 欧美日韩视频| 淫香欲色| 国产免费观看视频| 欧美又粗又大| 天天拍夜夜操| 中文字幕高清在线中文字幕中文字幕 | 欧美日韩在线免费| 91白浆| 性爱无码AV| 五月婷婷综合激情| 蜜臀av网站| 日韩图色| 国产午夜无码视频在线观看| 国产欧美日韩在线| 欧美操逼免费视频| 亚洲熟妇AV日韩熟妇在线| 日韩亚洲中文字幕| 日韩免费高清视频| 日本高清色清di免费观看| 欧美日韩一区二区三区视频| 黄色无码在线观看| 亚洲午夜在线观看| 成人午夜免费视频| av在线影院| 国产精品无码无套在线照片| 九九视频免费观看| 99这里只有精品视频| 2025AV中文字幕| 黄色福利网| 婷婷国产视频| 正在播放国产精品| 亚洲福利女神成人福利| 国产欧美综合在线观看| 亚洲视频在线免费看| 俺也干| 懂色中文字幕| 一区二区免费在线观看| 国产做受91电影| 涩久久| 秋霞一区二区三区无码| 手机看片欧美+日韩+国产| 亚洲人操逼视频| 92无码| 在线日韩中文字幕| 丰满熟妇高潮呻吟无码| 狠狠狠狠狠狠狠| 国产伦精品一区二区三区妓女| 69av电影| 四虎日韩| 在线黄| 欧美曰皮免费看| 久久毛片基地| 懂色AV无码中字幕一区| 亚洲污| 成人福利在线| 久艹在线观看视频| 三级片视频在线观看| 成人在线精品视频| 亚洲高清无码久久| 亚洲精品影视| 成人AV中文解说水果派| 黄色大片AV在线| 懂色AV无码中字幕一区| 天堂а√在线中文在线新版| 91人妻无码| 日韩一级在线视频| 97国产精品人人爽人人做| 蝌蚪窝在线观看| 亚洲日韩免费在线观看| 亚洲AV无码成人精品区在线欢看 | 乱伦无码| 日一区二区| 少妇白洁视频| 久热久| 三级片无码在线| 国产三级国产三级国产| 娇小,学生,高潮,videos| A级黄色毛片| 杨贵妃一级婬片90分钟| 国产区在线观看| 国产美女被爽到高潮免费A片软件| 亚洲美女操| 日韩一区二区视频| 国产亚洲欧美日韩高清| 91视频国产精品| 超碰自拍| 操屄在线视频| 亚洲第一免费视频| 蜜臀伊人| 在线亚洲一区| 国产h视频在线观看| 人妻超碰在线| 国产性爱自拍视频| 中文字幕中文字幕| 嫩BBB搡BBBB搡BBBB| 911香蕉视频| 韩国毛片| A级视频免费观看| 黑人大荫蒂女同互磨| 久草黄色电影在线观看| 国产激情一区二区三区| 亚洲精品在线观看视频| 无码成人毛片| 欧美日韩大片| 中文字幕综合网| 国产一级二级三级久久久| 伊人天天操| 欧美视频免费操逼图。| 午夜福利站| 亚洲精品AⅤ一区二| 青青草综合视频| a级毛片在线观看| 亚洲AV无码乱码| 国产天堂在线观看| 17c.白丝喷水自慰| 国产高清无码免费视频| 中文字幕高清无码在线观看| 国产成人Av| 国产亚洲无码激情| 波多野结衣亚洲无码| 久久精品国产AV一区二区三区 | 欧美五区| 国内免费AV| www.爆操| 国产精品无码免费视频| 精品乱子伦| 97精品综合久久| 免费成人在线网站| 一本色道久久综合| www.日韩精品| 黄色视频免费观看国产| 五月丁香花| 国产精品久久久久久久久久二区三区| 午夜探花在线观看| 久久这里只有| jzzijzzij亚洲成熟少妇在线观看 九色蝌蚪9l视频蝌蚪9l视频成人熟妇 | 加勒比一区二区三区| 中文字幕观看在线| 免费的黄色录像| 亚洲成人无码网站| 日韩免费AV| 黄av在线| 国产亚洲色婷婷| 天堂AV无码AV| 亚洲人体视频| 亚洲综合免费观看高清完整版在线观| 91丝袜| 精品福利一区二区三区| 精品国产午夜福利在线观看| 日韩欧美在线观看| 国产操逼小视频| 91AV电影| 欧美人成人无码| 欧美在线不卡| 国产亚洲精品午夜福利巨大软件| 欧美一级黄色电影| 人人射人人射| 国产丨熟女丨国产熟女视频| 老司机午夜免费精品视频| 黄色影片在线观看| 婷婷五月天成人社区| 99国产精品免费视频观看8| 五月激情黄色| 99热er| 成人乱码一区二区三区| 刘玥无码| 少妇白浆| 蜜臀av一区二区| 欧美成人社区| 大香蕉久久久久久| 中文字幕66页| 最新中文字幕免费MV第一季歌词| 99久久精品一区二区成人| 日韩高清在线播放| 亚洲人成免费网站| 91农村站街老熟女露脸| 成人国产| 91在线网址| 在线aⅴ| 亚洲无码999| 亚洲成人情趣大香蕉| 免费乱伦视频| 色色射| 婷婷丁香激情| 亚洲女同在线| 先锋资源av| 黄色永久免费| 五月婷婷激情| 性中国熟妇| 辽宁模特张雪馨视频最新| 婷婷99狠狠躁天天| 日本少妇黄色视频| 嫩BBB槡BBBB搡BBBB视频| 337P大胆粉嫩噜噜噜| 中文字幕视频网站| 日韩AV免费在线播放| 麻豆视频一区二区| 中国12一13毛片| 欧美色图在线播放| 欧美69影院| 91AV电影| 最新中文字幕在线观看视频| 天天天天毛片| 精品国产三级片| 免费观看黄色成人网站| 激情五月天网| 国产系列精品AV| JUY-579被丈夫的上司侵犯后的第7天,我| 99久久婷婷国产综合精品| 久久国产综合| 久久久国产一区二区三区| 97精品国产97久久久久久免费| 精品国产成人| 欧美囗交荫蒂AAAA| 先锋影音资源站| 久久久高清无码| 天天插在线视频| 中文熟女| 五月丁香色色网| 麻豆啪啪| 亚洲精品久久久久毛片A级绿茶| 日韩23岁观看| 国产无套免费网站69| 性欧美一区二区| 国产精品无码专区| 亚洲最大网站| 国产成人性| 热久久9| 五月天国产视频| av免费在线播放| 91人妻人人澡人人添人人爽| 亚洲中字幕新| 色汉综合| 久久撸在线视频| 国产精品久久久久永久免费看| 成人网站欧美| 久久久亚洲AV无码精品色午夜| 亚洲色图成人网| 久久久女人| 国产激情在线视频| 一二三区免费视频| 成人在线看片| 影音先锋天堂| 国产丝袜自拍| 日韩AV无码免费| 尤物免费视频| 欧美日韩日逼视频| 女侠吕四娘第二部| 无码成人AV| 99视频在线免费播放| 中文字幕不卡+婷婷五月| 亚洲欧美影院| AV在线播放中文字幕| AV无码免费观看| 毛片网站视频| 婷婷色色五月天图片| 亚洲视频久久| 亚洲精品无码免费| 天堂亚洲AV无码精品成人| 青春草在线观看国产| 久久久精品一区| 青娱乐精品视频| 波多野结衣av中文字幕| 国产精品国产三级国产专区52 | 人人插人人摸| 久久6精品| 日韩免费在线观看| 免费无码高清视频| 91成人福利视频| A视频免费在线观看| 欧美黄色一级视频| 成年人在线播放| 国产成人精品国内自产拍免费看| 青娱乐偷拍| 中文字幕在线观看视频免费| 91成人在线影院| 日韩va| 成人视频一区| 毛片A片免费看| 成人毛片网| 无码人妻精品一区二区三区99仓 | 麻豆国产91在线播放| 日本中文字幕精品| 色婷婷视频| 爱爱爱免费视频| 久久AA| 手机免费Av| 一本色道久久综合熟妇| 爱爱爱爱视频| 综合激情av| 亚洲AV成人片无码网站| 久草久久| 久久久国产精品在线| 日本三级网址| 精品人妻一区二区免费蜜桃视频| 在线观看成人18| 手机在线看片av| 免费看一区二区三区A片| 神马影院午夜福利| 国产av日韩av| 最美孕交vivoestv另类| 乌克兰毛片| 中文字幕成人在线观看| 久久久久久久久免费视频| 大香蕉一区二区| 国产一级AV国产免费| 性生活无码视频| 无码一区二区av| 亚洲理论电影| 亚洲黄色视频免费看| 在线观看亚| 51XX嘿嘿午夜| 老熟女痒到不行-ThePorn| 人人香蕉| 久久国产精品电影| 成人先锋AV| 五月天福利视频| 狠狠躁日日躁夜夜躁2022麻豆| 韩国一级AV| 青草伊人av| 五月婷婷色色| 特级西西444www精品视频| 99av| 操骚B| 免费A级毛片在线播放不收费| 一级二级三级无码| 日韩三级精品| 日韩一级性爱| 国产激情av| 性满足BBWBBWBBW| 国产女人18毛片水真多成人如厕 | 亚洲精品久久久久毛片A级牛奶| 日韩国产在线| 人妻精品一区二区| 国产精品91在线| 一区四区视频| 五月天亚洲激情| 国产欧美日韩在线| 欧美操逼视频网站| 欧美精品性爱视频| 免费观看黄片网站| 日韩在线一级片| 免费在线观看黄色视频网站| 欧美成人激情视频| 北条麻妃JUX-869无码播放| 国产成人V在线精品一区| 久久久久久97电影院电影院无码 | 粉嫩99国产精品久久久久久人妻| 正在播放李彩斐被洋老外| 图片区视频区小说区| 日本少妇BBW| 18禁一区二区| 97av在线| 2025中文在线观看最好看的电影| 日本www色| 北条麻妃无码播放| 日韩久久人妻| 精品三级网站| www.wuma| 台湾省成人网站| 91亚洲成人| 97视频在线观看免费| 少妇厨房愉情理伦BD在线观看| 亚洲AV国产| 亚洲图片在线播放| 91麻豆精品成人一区二区| 思思热99| 欧美视频a| 国产成人精品一区二区三区视频 | 国产美女被爽到高潮免费A片软件| 亚洲乱码国产乱码精品天美传媒| 韩国毛片| 久久久久久成人电影| 午夜褔利| 熟女国产| 黄色免费视频| 特级西西| 天天天天毛片| 中文字幕乱伦| 97久久久| 99精品久久久久久无码| 裸体美女视频欧美18| 亚洲天堂在线观看免费视频| 爱爱亚洲| 六月丁香久久| AV大香蕉| 欧美黄片在线| 在线永久看片免费的视频| 国产麻豆三级片| 丁香六月综合激情| 色撸撸在线视频| 成人一级黄色电影| 国产美女18毛片水真多| 强奸校花到高潮| 久久久黄色| 欧美日视频| 国产一级A片久久久免费看快餐 | 日本三级在线| 乱伦视频网站| 久久99嫩草熟妇人妻蜜臀| 大鸡巴免费视频| 日韩精品成人av| 午夜福利在线播放| 午夜激情操一操| 国产精品无码激情视频| 中文爱爱视频| 日韩A| 国产乱婬AAAA片视频| 在线观看你懂得| 精品成人A片久久久久久不卡三区| 91精品久久久久久久久久| 伊大香蕉| 蜜芽成人精品久久久视频| 亚洲AV永久无码精品国产精| 操综合| 91大神在线资源观看无广告| 国产777| 69av在线播放| 欧美黄片免费观看| 肉乳无码A片av| 天天综合干| 亚洲成人影音| 大骚逼影院| 少妇精品久久久久久久久久| 日韩香蕉网| 欧洲AV片| 夜夜撸天天日| 黄色的视频网站| 大屌av| 五丁香在线观看AV| 日本一级一片免费视频| 蜜桃av秘无码一区三区四| 91国产爽黄| 老司机无码视频| 五月天色综合| 日韩无码免费播放| 99热在线观看精品免费| 三级片网站在线播放| 一本色道久久综合无码人妻软件 | 国产做受精品网站在线观看| 懂色av粉嫩AV蜜臀AV| 综合婷婷| 伊人在线观看视频| 逼特逼在线观看| 国产AV一级| 国产精品久久久精品| 免费一级黄色视频| 亚洲成人性爱网| 成人特级毛片全部免费播放| 7777影视电视剧在线观看官网| 91在线网址| 手机毛片| 91丨九色丨老农村| 亚洲精品久久久久avwww潮水| 超碰97av| 色视频在线观看免费| 上床视频网站| 国产高清成人| 天天天天天天天干| 12—13女人毛片毛片| 国产主播第一页| 男人视频网| 国产激情视频网站| 免费无码毛片一区二区A片| 亚洲最大网站| 国内精品久久久久久久久久变脸 | 黄色视频网站国产| 中文日韩在线| 日本不卡在线视频| 国产日韩欧美视频| 无码国产视频| 东京热av在线| 亚洲AV无码免费| 东京热国产| 日韩午夜剧场| 无码视频观看| 日韩成人在线播放| 中文字幕av无码| 91蝌蚪丨人妻丨丝袜| 免费a片在线观看| 婷婷99| 亚洲热视频| 精品国产乱子伦一区二区三区最新章 | 亚州AV| 加勒比无码在线| 欧美色图88| 91视频在线免费观看| 亚洲第一成人网站| 国产一级美女操逼视频免费播放| AV日逼网| 久久精品禁一区二区三区四区五区| 免费精品黄色网页| 亚洲天堂av在线免费观看| 蜜桃高清无码| 爱爱免费视频| 宅男噜| 黄色小视频在线| 亚洲一区二区免费视频| 黑人亚洲娇小videos∞| 天天躁夜夜躁av| 粉嫩99精品99久久久久| 蜜桃AV无码一区二区三区| 日韩欧美成人网站| 丰满岳乱妇一区二区三区全文阅读 | 国产免费av在线观看| 亚洲美女网站| 成人毛片视频网站| 97午夜福利视频| 国产精品国产精品国产专区不52 | 人人草人人看| 国产福利在线观看| 亚洲天堂无码视频| 91精品久久久久久久| 一区二区三区久久久| 动图综合亚洲综合欧美男男| 精品中文在线视频| 亚洲成人在线视频观看| 亚洲精品乱码在线| 美女网站永久免费观看| 屁屁影院CCYYCOM国产| 国产99精品视频| 少妇无码在线观看| 国产av不卡| 91精品视频网站| 波多野结衣亚洲| 嫩草视频| 人妻日韩精品中文字幕| 国精久久久久| 中文字幕线观看| 欧美一级性爱在线观看| 亚洲一区二区免费视频| 日韩成人免费观看| 成人电影久久久| 成人a视频| 操少妇| 青娱乐国产在线| 夜夜骚精品人妻av一区| 亚州精品国产精品乱码不99勇敢 | 欧洲毛片基地c区| 国产一区二区三区视频在线| 久久久久亚洲AV无码成人片 | 亚洲无码在线精品| 国产三级无码视频| 麻豆影音先锋| 三级片一区二区| 性久久久久久| 青青草超碰| 日本久久久久久久久视频在线观看| 国产一精品| 在线观看亚洲无码视频| 豆花视频免费| 国产丝袜人妖TS系列| 能看的AV网站| 国产精品永久| 成人激情在线| 午夜操p| 欧美成人福利视频| 五月天婷婷综合网| 婷婷国产视频| 91在线无码精品秘入口国战| 亚洲日韩在线观看视频| 一区免费视频| 亚洲日本三级片| 激情a| 午夜天堂在线| 丰满人妻一区二区三区免费| 微拍福利一区| 日韩精品五区| 久草在线| 麻豆疯狂做受XXXX高潮视频| 欧美性爱A片| 亚洲成人免费在线| 国产第二页| 欧美精产国品一二三产品在哪买| 懂色AV| 日韩人妻精品一区二区| 免费观看黄色AV| 黄色电影A| 91精品大屁股白浆自慰久久久| 蜜桃久久99精品久久久酒店| 狠狠的操| 综合久久av| 中文字幕无码A片| 亚洲无码AV片| 成人亚洲AV日韩AV无码| 97精品| 五月丁香六月久久| 99精品网站| 欧美3p视频| 日韩欧美二区| 成人三级在线| 亚洲色逼图片| 一区二区av在线| 爱爱午夜福利| 久热国产精品| 亚洲AV成人无码| 国产熟女一区二区三区五月婷| 色噜噜网站| 中文字幕AV播放| 成人三级视频在线| 中文字幕在线一区二区a| 亚洲无码激情在线| 中文字幕中文字幕| 欧美A在线观看| 人妻体内射精一区二区三区| 伊人成色| 无码中文字幕高清| 久久免费9| 婷婷国产视频| 国产一级a一级a免费视频| 亚洲av电影在线观看| 五月播播| 日本一级特级毛片视频| 国产精品美女视频| 日韩中文字幕AV| 天天操天天干欧美精品| 我要看黄色一级片| 午夜AV福利| 爱逼AV| 丁香婷婷五月| 婷婷丁香五月花| 男女乱伦视频| 不卡视频在线观看| 豆花视频在线| 欧美精品欧美精品系列| 激情无码精品| 欧美色色色色色| 丁香五月在线观看| 亚洲色婷婷久久精品AV蜜桃| 精品视频网站| 欧美人妻激情| 午夜免费视频| 亚洲成人无码片| 亚洲伊人大香蕉| 色色色777| 欧美无人区码suv| 日韩中字无码| 久草免费在线观看视频| 成人爽a毛片一区二区免费| 91一区二区| 97av在线| 中文资源在线a中文| 专区无日本视频高清8| 综合欧美国产视频二区| AV中文无码| 五月丁香婷婷综合网| 国产乱伦免费| 久久澡| 特黄AAAAAAAA片免费直播| 国产黄色视频免费观看| 中文字幕免费在线看一区七区| 日韩高清无码免费观看| 手机在线看A片| 日韩视频免费在线观看| 亚洲乱论| 日韩一区二区三区精品| av无码一区| 亚洲福利视频97| 日韩人妻久久| 亚洲欧美日韩色图| 成人无码精品| 亚洲成人动漫在线| 亚洲AV成人无码AV小说| 中文字幕国产视频| 成人区色情综合小说| 一级黄色生活片| 男人色天堂网| 淫荡五月天视频导航| 一区二区三区Av| 国产三级国产三级国产| 天堂a中文在线| 深爱五月天| 亚洲精品久久久蜜桃| 亚洲性爱电影| 国产精品色情A级毛片| 亚洲日韩欧美一区二区天天天| 91乱子伦国产乱子伦| 国产AV毛片| 大香煮伊在75| 成人美女视频| 国产色情在线| 亚洲天堂av在线观看| www黄色片| 爆操人妻| 日本三级片在线| 夜夜撸天天操| 内射在线| 无码高清| 亚洲日逼网站| 99热这里有精品| 久久大香蕉| 亚洲欧美在线视频观看| 色综合一区二区三区| 亚洲中字幕新| 黑人毛片91久久久久久| 99在线精品视频免费观看20| 精品一区二区三区四区| 污视频网站免费观看| 在线播放无码| 国产一级片| 国产久久性爱| 极品AV| 欧美国产成人在线| 国产高清毛片| 中文字幕AV一区| 亚洲无码免费在线| 日韩啪啪片| 亚洲av偷拍| 超碰99在线| 亚洲天堂一区在线观看| 无码成人A片在线观看| 午夜无码鲁丝片午夜精品| 国产亚洲精品成人a| 色色五月天婷婷| 久久久精品久久| 日韩精品一二区| 黄片网站免费观看| 97久久超碰| 伊人久久免费视频| 污视频在线免费| 欧美自拍视频在线| 欧美爱爱网| 久久a视频|