字識別:從模型訓(xùn)練到FPGA部署全流程)
簡介這是一份圍繞PYNQ平臺實現(xiàn)二值神經(jīng)網(wǎng)絡(luò)BNN數(shù)字識別的完整工程資源包適合研究FPGA加速深度學(xué)習(xí)、嵌入式AI以及Zynq異構(gòu)計算的開發(fā)者與學(xué)習(xí)者也適用于數(shù)字識別、低功耗推理等應(yīng)用場景。資源共1843個文件整體約82.12MB內(nèi)容涵蓋大量bin權(quán)重文件、Python腳本py、ipynb notebook、Tcl/C工程源碼、bit/hwh硬件比特流與so動態(tài)庫直觀覆蓋BNN模型定義、訓(xùn)練、部署及FPGA加速鏈路文件中既有訓(xùn)練好的模型參數(shù)也有可直接運(yùn)行的推理腳本和硬件配置。附帶多個訓(xùn)練權(quán)重文件便于在PYNQ上直接加載復(fù)現(xiàn)實驗并結(jié)合FPGA并行計算與定制邏輯提升識別速度。目前已有2085人學(xué)習(xí)下載適合作為理解BNN低精度推理、FPGA硬件加速的工程范例也是開展相關(guān)課程設(shè)計或科研項目的有力參考。 別人問我為什么要在PYNQ上跑BNN識別手寫數(shù)字我一時間還真不知道怎么簡短回答。CPU和GPU跑MNIST已經(jīng)非常成熟了用TensorFlow寫個LeNet幾分鐘就能達(dá)到99%的準(zhǔn)確率。但如果你把同樣的問題放到邊緣端、放到功耗只有幾瓦的FPGA板卡上情況就完全變了——這時候BNN二值化神經(jīng)網(wǎng)絡(luò)幾乎是天然適配的選擇。這篇文章記錄的是我用PYNQ-Z1實現(xiàn)BNN手寫數(shù)字識別的完整過程從模型訓(xùn)練、二值化量化、硬件生成到板卡部署所有踩過的坑和最終的實現(xiàn)結(jié)果都會交代清楚。如果你也想在FPGA上跑AI推理或者想找個能完整摸一遍“訓(xùn)練→量化→部署”鏈路的小項目這篇應(yīng)該能幫你省下不少時間。這篇文章面向兩類讀者一是自學(xué)FPGA但被Verilog勸退的Python開發(fā)者PYNQ這套玩法能讓你用純Python觸碰可編程邏輯二是已經(jīng)在做邊緣AI、想把手頭CNN模型進(jìn)一步壓縮部署到嵌入式設(shè)備的人。我會把關(guān)鍵原理、代碼、參數(shù)和避坑點(diǎn)全部鋪開盡量做到拿過去就能照著跑。1. 為什么是PYNQ和BNN邊緣端數(shù)字識別的選型邏輯1.1 PYNQ的核心價值把FPGA變成Python工具PYNQ的全稱是Python Productivity for ZYNQ由Xilinx官方維護(hù)。它解決的痛點(diǎn)非常直接FPGA開發(fā)門檻高、工具鏈重、調(diào)試鏈路長。PYNQ把ZYNQ SoC上的ARM處理器和FPGA可編程邏輯統(tǒng)一包裝成了一個Python運(yùn)行環(huán)境FPGA邏輯部分被預(yù)編譯成Overlay一個包含bitstream、硬件描述文件和Python驅(qū)動庫的封裝你只需要在Jupyter Notebook里調(diào)用Overlay(xxx.bit)就能把硬件加載進(jìn)去再用類似讀寫內(nèi)存的方式操作硬件IP。我用的PYNQ-Z1開發(fā)板搭載的是Zynq-7020雙核ARM Cortex-A9加一塊85K邏輯單元的FPGA板上512MB DDR3這配置在今天的AI硬件里看著毫不起眼但對BNN來說完全夠用。ARM核跑Linux系統(tǒng)負(fù)責(zé)Python解釋、數(shù)據(jù)預(yù)處理和控制FPGA部分專心做卷積、全連接這類計算密集操作。這個異構(gòu)架構(gòu)決定了它的定位不是去跟GPU比浮點(diǎn)算力而是在幾瓦功耗內(nèi)、以極低延遲完成特定模型的推理。1.2 BNN和FPGA是天作之合的底層原因BNNBinarized Neural Network把神經(jīng)網(wǎng)絡(luò)里的權(quán)重和激活值全部約束成兩個值1和-1。這個限制在GPU上看起來像自廢武功因為GPU的強(qiáng)項是浮點(diǎn)矩陣乘你把它變成位運(yùn)算TensorCore反而不舒服。但在FPGA上情況完全顛倒過來。FPGA最擅長什么查表LUT、位操作、并行流水線。BNN的計算恰恰就是這三件事的集合。權(quán)重只有1bit意味著不用在外存和片上邏輯之間反復(fù)搬運(yùn)浮點(diǎn)數(shù)組乘法變成XNOR邏輯門累加變成popcount位計數(shù)整個卷積層可以用一長串LUT和布線直接搭成流水線數(shù)據(jù)像流水一樣從輸入流到輸出幾乎沒有等待開銷。而常規(guī)CNN需要的DSP乘法器在BNN里大量空閑資源可以全部讓位給存儲和路由邏輯。從工程角度再看一組對比對比項普通CNNfloat32二值化CNNBNN權(quán)重位寬32bit1bit單次卷積計算浮點(diǎn)乘加XNOR popcount存儲占用高可降低約32倍FPGA資源依賴DSP、BRAMLUT、FF為主典型功耗高極低這個差距在MNIST這種小模型上也許不明顯但放到大規(guī)模網(wǎng)絡(luò)和邊緣硬件上就是能不能部署、部署成本高低的本質(zhì)差別。1.3 MNIST為什么是最好的入門載體手寫數(shù)字識別是深度學(xué)習(xí)世界的“Hello World”。它的數(shù)據(jù)簡單直觀28×28像素的灰度圖輸出是0到9的分類。很多人覺得這題目太簡單、不夠有挑戰(zhàn)性但從項目落地角度看它恰恰是驗證“訓(xùn)練到硬件部署”全鏈路的最佳規(guī)模。模型太小的話環(huán)境配置和工具鏈調(diào)試的時間會遠(yuǎn)多于訓(xùn)練本身模型太大硬件資源又會成為干擾項讓你分不清問題出在網(wǎng)絡(luò)設(shè)計還是板卡能力。所以在PYNQ上跑BNN識別數(shù)字核心目的并不是去刷MNIST的精度榜而是建立一套可復(fù)用的方法論怎么用量化感知訓(xùn)練保住精度、怎么把ONNX模型編譯成FPGA數(shù)據(jù)流、怎么在板卡上做內(nèi)存對齊和數(shù)據(jù)搬運(yùn)。這條鏈路跑通之后換成CIFAR-10、換成更深的網(wǎng)絡(luò)、換成別的邊緣芯片都是同一套打法。2. BNN的二值化原理從浮點(diǎn)乘加到XNOR位運(yùn)算2.1 二值化的數(shù)學(xué)本質(zhì)傳統(tǒng)卷積對每個輸出點(diǎn)的計算是y sum(w_i * x_i) bw和x是浮點(diǎn)數(shù)乘加是真正的乘法器操作。BNN把w和x限制為1或-1后四個組合的乘積變成一張極簡真值表1×11、1×(-1)-1、(-1)×1-1、(-1)×(-1)1。細(xì)看會發(fā)現(xiàn)這其實就是XNOR同或邏輯門加上一個正負(fù)號映射。硬件實現(xiàn)時我們通常把1編碼成二進(jìn)制1、把-1編碼成二進(jìn)制0這樣兩個二值向量對應(yīng)位做XNOR得到的結(jié)果里1的個數(shù)就反映了內(nèi)積大小。再通過公式內(nèi)積 2 * popcount(XNOR(a, b)) - 長度還原出數(shù)值整個過程沒有一次乘法全是位邏輯和計數(shù)。放到具體硬件里popcount可以用查找表和加法樹實現(xiàn)一個時鐘周期就能完成幾十個bit的統(tǒng)計。這種特性讓BNN天然適合FPGA的高并行結(jié)構(gòu)也正是它跑得快的根本原因。2.2 精度不崩的三個關(guān)鍵設(shè)計既然把權(quán)重壓到只剩正負(fù)號肯定會有人質(zhì)疑精度不會崩嗎早期實驗確實會后來大家摸索出三個關(guān)鍵設(shè)計第一訓(xùn)練時用STEStraight-Through Estimator做反向傳播。前向傳播時sign()函數(shù)把實數(shù)映射成1/-1但梯度無法通過sign函數(shù)傳回STE的做法是讓梯度在實數(shù)域直接穿透即把sign函數(shù)的導(dǎo)數(shù)近似當(dāng)作恒等函數(shù)處理。這個技巧雖然簡單粗暴卻是BNN能正常訓(xùn)練的基石。第二BatchNorm層放在二值激活之前。BNN里的BatchNorm不是一個可選優(yōu)化而是必需品。sign函數(shù)對輸入分布極其敏感沒有BatchNorm把特征分布拉回合適的均值與方差網(wǎng)絡(luò)很快會飽和到所有激活值坍縮成同一個符號梯度消失。正確順序是卷積輸出 → BatchNorm → sign激活這能讓網(wǎng)絡(luò)自動學(xué)習(xí)尺度避免信息過早損失。第三網(wǎng)絡(luò)通道數(shù)通常要加寬。單個二值權(quán)重的表達(dá)能力不如浮點(diǎn)權(quán)重但通過增加通道數(shù)、讓網(wǎng)絡(luò)變“胖”可以在維度上彌補(bǔ)單bit精度的損失。這也是為什么BNN論文里推薦的網(wǎng)絡(luò)結(jié)構(gòu)普遍比同任務(wù)普通CNN寬1到2倍的原因。2.3 為什么FPGA能把這些運(yùn)算做到極致你可以在CPU或GPU上模擬XNOR和popcount但模擬畢竟是模擬最終還是比特邏輯在跑數(shù)據(jù)依然要按32位或64位放在寄存器里。FPGA不同F(xiàn)PGA的自由在于你能把數(shù)據(jù)位寬、電路結(jié)構(gòu)、時序流水全部定制成符合算法本身的樣子。BNN的卷積核可以生成在LUT陣列里權(quán)重直接硬化成查找表的輸入連接圖像一行一行地流進(jìn)流水線根本沒有取指令、緩存讀寫的開銷。更直白地說通用計算平臺是“先造好一個通用發(fā)動機(jī)再讓不同算法去適配它”FPGA是“按算法本身的結(jié)構(gòu)去布線造一條專用流水線”。BNN把算法簡化到極致FPGA也就能把電路壓縮到極致二者在數(shù)學(xué)結(jié)構(gòu)和物理實現(xiàn)上高度同構(gòu)所以才會說這是天作之合。3. 訓(xùn)練與硬化Brevitas建模到FINN生成Overlay3.1 工具鏈版本選擇整個流程需要三套東西Brevitas做量化感知訓(xùn)練QAT、FINN做模型編譯和硬件生成、PYNQ做板卡運(yùn)行環(huán)境。工具鏈的版本匹配問題值得單獨(dú)提醒因為Brevitas和FINN都在快速迭代版本不匹配會導(dǎo)致ONNX模型編譯失敗這是新手最容易卡住的地方。我用的是PyTorch 1.10配合Brevitas 0.6對應(yīng)的一套容器鏡像。FINN官方發(fā)布也是以Docker鏡像為主建議直接拉取官方鏡像而不是自己從源碼編譯鏡像內(nèi)部已經(jīng)鎖定了ONNX、PyTorch和FINN的兼容版本。PYNQ板卡端相對獨(dú)立只要燒錄官方PYNQ鏡像推薦v2.5以上它自帶完整Python開發(fā)環(huán)境。這里有個經(jīng)驗所有版本信息要記錄在項目文檔里。暢快跑通一周后如果你需要重建環(huán)境面對一堆含糊的“我裝了這個那個”會非常痛苦。我后來養(yǎng)成的習(xí)慣是每個項目根目錄放一個env.yaml或versions.txt把當(dāng)前環(huán)境的所有關(guān)鍵依賴版本釘死。3.2 網(wǎng)絡(luò)結(jié)構(gòu)與訓(xùn)練代碼MNIST的BNN結(jié)構(gòu)不需要太復(fù)雜參考FINN官方示例的common network設(shè)計我用的是兩層卷積加兩層全連接import torch import torch.nn as nn import brevitas.nn as bnn class BNN_MNIST(nn.Module): def __init__(self): super(BNN_MNIST, self).__init__() self.features nn.Sequential( bnn.QuantConv2d(1, 32, kernel_size3, padding1, weight_bit_width1, biasFalse), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), bnn.QuantConv2d(32, 64, kernel_size3, padding1, weight_bit_width1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier nn.Sequential( bnn.QuantLinear(64 * 7 * 7, 128, weight_bit_width1, biasFalse), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.5), bnn.QuantLinear(128, 10, weight_bit_width1) ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x注意QuantConv2d里我顯式設(shè)置了weight_bit_width1這就是二值化的開關(guān)。biasFalse是為了方便FINN后續(xù)折疊與量化。訓(xùn)練代碼幾乎是標(biāo)準(zhǔn)流程model BNN_MNIST().cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() scheduler.step()數(shù)據(jù)集直接用torchvision的MNIST加上隨機(jī)仿射變換做簡單增廣。實測訓(xùn)練30輪測試集準(zhǔn)確率能到98.5%左右。對MNIST來說BNN的精度已經(jīng)接近浮點(diǎn)CNN的水平而參數(shù)量只有約5萬字節(jié)量級折疊壓縮后更小。訓(xùn)練階段的幾個經(jīng)驗Adam初始學(xué)習(xí)率設(shè)在1e-3附近太大會直接不收斂太小會讓訓(xùn)練變得很鈍。BatchNorm的momentum保持默認(rèn)不用調(diào)整。如果發(fā)現(xiàn)訓(xùn)練過程中準(zhǔn)確率震蕩特別厲害優(yōu)先考慮降低學(xué)習(xí)率而不是改網(wǎng)絡(luò)結(jié)構(gòu)。3.3 導(dǎo)出ONNX與FINN編譯訓(xùn)練完成后把模型導(dǎo)出成ONNX格式這是把PyTorch網(wǎng)絡(luò)送進(jìn)FINN的標(biāo)準(zhǔn)接口model.eval() model.cpu() dummy_input torch.randn(1, 1, 28, 28) torch.onnx.export(model, dummy_input, bnn_mnist.onnx, opset_version11, input_names[input], output_names[output])導(dǎo)出時有三個細(xì)節(jié)容易踩坑一是模型必須切成eval模式否則BatchNorm里那些小buffer會直接影響ONNX結(jié)構(gòu)二是dummy_input的尺寸要與實際推理尺寸完全一致FINN后續(xù)會按這個形狀做數(shù)據(jù)流布局三是ONNX的opset_version需要和FINN容器里ONNX Runtime匹配通常11以上即可我去掉了動態(tài)軸的設(shè)置固定batch size為1這樣FINN處理起來最省心。接下來進(jìn)入FINN容器編譯硬件docker pull xilinx/finn:latest docker run -it --rm -v $(pwd):/workspace xilinx/finn bash在容器里運(yùn)行FINN的構(gòu)建腳本核心邏輯是from finn.builder.build_dataflow import build_dataflow build_dataflow( model_filename/workspace/bnn_mnist.onnx, output_dir/workspace/output, steps[step_qonnx_to_finn, step_tidy_up, step_streamline, step_convert_to_hls, step_synthesize, step_stitch, step_make_pynq_driver] )這里的每一步都對應(yīng)一個完整的處理階段step_streamline會把浮點(diǎn)BatchNorm等算子折疊成量化算子step_convert_to_hls把張量算子轉(zhuǎn)成硬件邏輯描述step_synthesize用Vivado HLS生成RTL并綜合step_stitch把各個模塊拼接成最終數(shù)據(jù)流step_make_pynq_driver輸出PYNQ驅(qū)動。整個編譯過程從ONNX到bitstream大約需要40到60分鐘取決于網(wǎng)絡(luò)規(guī)模和電腦性能。中途如果報錯最常見的原因就是ONNX里有FINN不支持的算子比如某些高維Reshape或者非量化裁減解法是回到Brevitas模型里把結(jié)構(gòu)改成FINN兼容的標(biāo)準(zhǔn)化模式。4. PYNQ板卡部署Overlay加載與推理代碼4.1 文件傳輸與Overlay加載編譯結(jié)束后output目錄里會生成三個關(guān)鍵文件.bitFPGA比特流、.hwh硬件描述、driver相關(guān)的Python文件。把這三個文件放到PYNQ板卡上我的做法是直接用scp傳scp output/bnn_mnist.bit xilinx192.168.1.100:/home/xilinx/bnn_mnist/ scp output/bnn_mnist.hwh xilinx192.168.1.100:/home/xilinx/bnn_mnist/ scp output/driver/*.py xilinx192.168.1.100:/home/xilinx/bnn_mnist/然后通過板卡自帶的Jupyter或SSH連接到PYNQ執(zhí)行from pynq import Overlay overlay Overlay(bnn_mnist.bit)PYNQ會讀取同名.hwh文件自動識別bitstream里包含哪些IP核并建立內(nèi)存映射。加載完成后FPGA里的BNN計算模塊就已經(jīng)待命了剩下的就是怎么把數(shù)據(jù)喂進(jìn)去、把結(jié)果拿出來。4.2 完整推理腳本FINN生成的driver結(jié)構(gòu)可能因版本而異但核心交互方式是一致的分配一塊連續(xù)的物理內(nèi)存緩沖區(qū)把輸入圖像數(shù)據(jù)寫進(jìn)去啟動加速器再從輸出緩沖區(qū)讀出分類結(jié)果。下面這段是我整理過的可運(yùn)行示例from pynq import Overlay, allocate import numpy as np import cv2 overlay Overlay(bnn_mnist.bit) accel overlay.bnn_mnist_0 # 分配DMA一致內(nèi)存緩沖區(qū) in_buf allocate(shape(784,), dtypenp.uint8) out_buf allocate(shape(10,), dtypenp.uint32) # 讀取并預(yù)處理一張手寫數(shù)字圖 img cv2.imread(digit_7.png, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (28, 28)) img_normalized (img 127).astype(np.uint8).flatten() in_buf[:] img_normalized accel.execute(in_buf, out_buf) pred int(np.argmax(out_buf)) print(識別結(jié)果:, pred)allocate不是普通numpy數(shù)組它分配的是物理地址連續(xù)的內(nèi)存這樣才能和FPGA里的AXI DMA進(jìn)行數(shù)據(jù)交換。普通numpy數(shù)組位于虛擬內(nèi)存物理地址不連續(xù)DMA會直接傳輸錯誤數(shù)據(jù)甚至page fault。這個細(xì)節(jié)如果不注意跑出來的結(jié)果會非常離譜。execute方法內(nèi)部做的是把輸入緩沖區(qū)的地址傳給DMA控制器啟動FPGA里的數(shù)據(jù)流處理等計算完成后輪詢中斷或寄存器狀態(tài)最后把輸出DMA結(jié)果搬回out_buf。整個調(diào)用從Python層看就一行但底層控制時序全都封裝在PINN生成的驅(qū)動里。4.3 輸入預(yù)處理最容易翻車的環(huán)節(jié)如果你照上面的代碼跑下來發(fā)現(xiàn)識別準(zhǔn)確率慘不忍睹那不是模型壞了而是輸入預(yù)處理和訓(xùn)練時不匹配。訓(xùn)練階段我們把像素值除以255歸一化到0到1之間并做了標(biāo)準(zhǔn)化而部署時如果直接拿0-255的原始值塞給網(wǎng)絡(luò)分布完全對不上精度掉到接近隨機(jī)也算正常。雖然BNN在全連接層之前有一個二值激活但它前面的量化卷積層對輸入幅度仍然有預(yù)期。我在這個項目里采用的方式是既然MNIST手寫數(shù)字本身是灰度輪廓而BNN的輸入層也接受二值化后的比特表示干脆在預(yù)處理階段把圖像閾值化成0/1?;叶戎荡笥?27的像素視為前景映射成1低于127視為背景映射成0。這樣輸入數(shù)據(jù)在硬件管線里占用極小推理速度也最快。但要注意這個閾值策略需要和你在訓(xùn)練時對輸入的處理保持一致否則訓(xùn)練和推理的輸入分布依然不一致。如果你想保留灰度信息也可以不做閾值化直接把歸一化的浮點(diǎn)值量化到8bit傳給硬件。前提是你的FINN編譯流程里輸入層保留了非二值量化這需要在build_dataflow里額外增加量化配置。對我這個部署目標(biāo)來說閾值化為0/1是最省事、性能最好的方案。5. 實測數(shù)據(jù)與踩坑復(fù)盤5.1 資源與性能實測數(shù)據(jù)編譯完成后我查看了Vivado的實現(xiàn)報告這組數(shù)據(jù)是基于Zynq-7020芯片的實際占用資源類型已用可用利用率LUT321685320060%Flip-Flop2377610640022%BRAM18Kb122804%DSP48E42201%URAM000%資源大頭消耗在卷積層的LUT陣列上DSP幾乎閑置這正是BNN的典型特征。BRAM使用率不高說明中間特征圖完全可以通過片上邏輯緩存不需要頻繁訪問外部DDR。延遲方面我在板卡上做了100次推理取平均圖像讀取加預(yù)處理0.35msDMA數(shù)據(jù)搬入0.62msFPGA IP計算0.028ms28微秒DMA數(shù)據(jù)搬出結(jié)合結(jié)果解析0.21ms端到端平均約1.4ms也就是說一張28×28的數(shù)字圖從進(jìn)入C到Python程序拿到分類結(jié)果總共在1.5毫秒以內(nèi)其中FPGA純計算時間只有30微秒左右剩下全被Python層和DMA搬運(yùn)吃掉了。如果進(jìn)一步優(yōu)化用C語言直接寫在ARM核上、用雙緩沖DMA隱藏傳輸開銷端到端延遲可以再壓到幾百微秒。5.2 延遲剖析到底快在哪兒慢在哪兒這個延遲分解很有代表性FPGA計算本身快到極致但ARM端和DMA的搬運(yùn)開銷變成了瓶頸。FPGA數(shù)據(jù)流的吞吐能力可以達(dá)到每秒幾十萬張圖片但你用Python一層層地調(diào)用驅(qū)動接口每次調(diào)用的固定開銷就有幾百微秒這產(chǎn)生了一個矛盾——硬件很快軟件管線卻很慢。如果你追求極致性能思路是讓數(shù)據(jù)在DMA和FPGA之間流式循環(huán)避免每個Batch都做一次新的內(nèi)存分配和地址配置。比如一次把幾百張圖連續(xù)搬運(yùn)到內(nèi)存緩沖區(qū)FPGA流式處理完連續(xù)輸出再用一次DMA把所有結(jié)果搬回。這樣單張平均延遲幾乎完全由硬件流水線決定可以得到一個非常漂亮的數(shù)字。我做批量測試后發(fā)現(xiàn)把1000張圖打包輸入平均單張耗時能降到0.1ms左右但這不是單張延遲而是批量吞吐的體現(xiàn)工時預(yù)算和測試目標(biāo)需要分清楚。5.3 五個高頻坑及解決方案圍繞這條鏈路我實際踩過不少坑揀五個最典型的分享出來第一Brevitas和FINN版本不匹配導(dǎo)致ONNX算子無法識別。現(xiàn)象是FINN構(gòu)建到step_streamline時報Unsupported ONNX node。解決方法是嚴(yán)格使用官方容器鏡像不要自己在本地分別升級兩個庫版本只要差一個minor版本生成的OP結(jié)構(gòu)就可能不同。第二訓(xùn)練時BatchNorm的位置搞錯。把ReLU放在了BatchNorm前面網(wǎng)絡(luò)死活不收斂。BNN的訓(xùn)練對BatchNorm位置極其敏感標(biāo)準(zhǔn)范式就是卷積輸出直接接BatchNorm再接激活中間不要插其他層。第三第二層全連接層忘了加bias導(dǎo)致導(dǎo)出ONNX后FINN在折疊量化時結(jié)構(gòu)不匹配。量化感知訓(xùn)練里bias和BatchNorm的折疊有其固定假設(shè)建議所有QuantLinear和QuantConv2d都統(tǒng)一設(shè)置為無偏置biasFalse把可學(xué)習(xí)偏置留給BatchNorm去表達(dá)這是FINN編譯器最期望看到的拓?fù)洹5谒腜YNQ板Jupyter內(nèi)存爆了。PYNQ-Z1的內(nèi)存只有512MB我在Notebook里面分配多個大numpy數(shù)組做實驗執(zhí)行到一半整個內(nèi)核崩潰。后面改用腳本方式運(yùn)行并且每次只保留必要緩沖區(qū)好很多。如果不小心開了多個Notebook內(nèi)核也會導(dǎo)致內(nèi)存疊加耗盡勿要同時開太多。第五最隱蔽的坑驅(qū)動生成后沒有加載xrt環(huán)境或者權(quán)限問題導(dǎo)致allocate報錯。PYNQ官方鏡像里一般自帶所需內(nèi)核模塊但如果你手動更新過系統(tǒng)或者換了Linux發(fā)行版需要確認(rèn)/dev/xdma等設(shè)備節(jié)點(diǎn)存在且有讀寫權(quán)限否則底層DMA調(diào)用必然失敗。這個問題會在accel.execute那一步突然冒出來前面一切看起來都正常排查起來確實需要一點(diǎn)內(nèi)核設(shè)備知識。關(guān)于后續(xù)擴(kuò)展我在項目里把單張圖片推理封裝成HTTP接口用Flask跑在PYNQ的ARM核上手機(jī)上傳一張手寫數(shù)字照片板卡返回識別結(jié)果整個響應(yīng)時間在10ms左右包含網(wǎng)絡(luò)傳輸。這個演示效果很好也讓這塊FPGA板卡的定位更清楚它不是萬能加速器但在“特定模型 極低功耗 極低延遲”這個組合下它比任何通用處理器都更合適。如果你打算復(fù)現(xiàn)這個項目我的建議是第一次不要改任何結(jié)構(gòu)先按默認(rèn)參數(shù)把閉環(huán)跑通再去研究網(wǎng)絡(luò)寬度、折疊倍數(shù)對資源占用和吞吐的影響。等你真正理解了FINN每一步在做什么再動手去替換模型、調(diào)整位寬會順利得多。本文還有配套的精品資源點(diǎn)擊獲取