Pytorch自定義C++/CUDA擴展

Pytorch自定義C++/CUDA擴展

翻譯自:官方文檔

PyTorch 提供了大量與神經網絡、張量代數、數據整理和其他操作。但是,我們有時會需要更加定制化的操作。例如,想要使用論文中找到的一種新型的激活函數,或者實現自己設計的算子。

在 PyTorch 中集成此類自定義操作的最簡單方法是使用 Python 擴展這里概述的 FunctionModule 。這里已經提供了自動微分的全部功能(無需編寫計算微分的函數)以及 Python 的常用的表達是。但是,有時算子更適合用 C++ 實現。例如,某些算子可能需要非常快,因為它在模型中被非常頻繁地調用,或者即使很少調用也非常耗時。另一個可能的原因是某些算子依賴于其他 C 或 C++ 庫或。為了解決這種情況,PyTorch 提供了一種編寫自定義 C++ 擴展的非常簡單的方法。

C++ 擴展允許用戶創建在 out-of-source 定義的 PyTorch 算子,即與 PyTorch 后端分離。這種方法不同于實現原生 PyTorch 操作的方式。 C++ 擴展旨在節省大量與將操作與 PyTorch 的后端集成相關的樣板,同時為基于 PyTorch 的項目提供高度的靈活性。然而,一旦將算子定義為 C++ 擴展,將其轉換為原生 PyTorch 函數主要是代碼組織問題,如果決定將操作貢獻給上游,則可以事后解決。

動機、實例與Python擴展實現

本文下面的部分將介紹一個編寫和使用 C++(和 CUDA)擴展的實例。

假設我們提出了一種新的循環單元,這個循環單元類似于 LSTM,但不同之處在于它沒有遺忘門,并使用指數線性單元 (ELU) 作為其內部激活函數。因為這個單元永遠不會忘記,我們稱之為 LLTM,或 Long-Long-Term-Memory 單元。

LLTM 與普通 LSTM 的兩種不同之處非常重要,以至于我們無法為我們的目的配置 PyTorch 的 LSTM 算子,因此我們必須創建一個自定義算子。第一個也是最簡單的方法——可能在所有情況下都是很好的第一步——是用 Python 在普通的 PyTorch 中實現我們想要的功能。為此,我們需要繼承 torch.nn.Module 并實現 LLTM 的 forward。即:

import torch
import math
import torch.nn.functional as Fclass LLTM(torch.nn.Module):def __init__(self, input_features, state_size):super(LLTM, self).__init__()self.input_features = input_featuresself.state_size = state_size# 3 * state_size for input gate, output gate and candidate cell gate.# input_features + state_size because we will multiply with [input, h].self.weights = torch.nn.Parameter(torch.empty(3 * state_size, input_features + state_size))self.bias = torch.nn.Parameter(torch.empty(3 * state_size))self.reset_parameters()def reset_parameters(self):stdv = 1.0 / math.sqrt(self.state_size)for weight in self.parameters():weight.data.uniform_(-stdv, +stdv)def forward(self, input, state):old_h, old_cell = stateX = torch.cat([old_h, input], dim=1)# Compute the input, output and candidate cell gates with one MM.gate_weights = F.linear(X, self.weights, self.bias)# Split the combined gate weight matrix into its components.gates = gate_weights.chunk(3, dim=1)input_gate = torch.sigmoid(gates[0])output_gate = torch.sigmoid(gates[1])# Here we use an ELU instead of the usual tanh.candidate_cell = F.elu(gates[2])# Compute the new cell state.new_cell = old_cell + candidate_cell * input_gate# Compute the new hidden state and output.new_h = torch.tanh(new_cell) * output_gatereturn new_h, new_cell

然后我們可以這樣調用:

import torchbatch_size = 4
input_features = 12
state_size = 8X = torch.randn(batch_size, input_features)
h = torch.randn(batch_size, state_size)
C = torch.randn(batch_size, state_size)rnn = LLTM(input_features, state_size)new_h, new_C = rnn(X, (h, C))

當然,我們大多數時候應該使用上面這種方法來擴展 PyTorch。因為 PyTorch 對 CPU 和 GPU 的操作實現了高度優化,并由 NVIDIA cuDNN、Intel MKL 或 NNPACK 等庫提供支持,因此上述 PyTorch 代碼通常足夠快。但是,在某些情況下還有進一步改進性能的空間。最明顯的原因是 PyTorch 不知道我們正在實現的算法。它只知道我們用來組成算法的各個操作。因此,PyTorch 必須一個接一個地單獨執行我們指定的操作。由于對操作的實現(或內核)的每個單獨調用(可能涉及啟動 CUDA 內核)都有一定的開銷,這種開銷在許多函數調用中可能會變得很重要。此外,Python 解釋器本身可能會減慢我們的程序。

因此,一種加快速度的明確方法是用 C++(或 CUDA)重寫部分并融合特定的操作組合。融合意味著將許多函數的實現組合成一個函數,這會啟動更少的內核以及我們可以通過提高全局數據流的可見性來執行的其他優化。

接下來我們使用 C++ 擴展來實現 LLTM 的融合版本。我們將從使用純 C++ 編寫它開始,使用為 PyTorch 的大部分后端提供支持的 ATen 庫。然后,我們將通過將模型的一部分移動到 CUDA 內核以從 GPU 提供的大規模并行性中受益,從而進一步加快速度。

實現C++擴展

C++ 擴展有兩種形式:它們可以使用

  • setuptools “ahead of time (AOT)” 構建,
  • 或者通過 torch.utils.cpp_extension.load() “just in time (JIT)” 構建。

我們將逐個介紹。

aot編譯擴展

對于“ahead of time”方式,我們通過編寫一個 setup.py 腳本來構建我們的 C++ 擴展,該腳本使用 setuptools 來編譯我們的 C++ 代碼。

from setuptools import setup, Extension
from torch.utils import cpp_extensionsetup(name='lltm_cpp',ext_modules=[cpp_extension.CppExtension('lltm_cpp', ['lltm.cpp'])],cmdclass={'build_ext': cpp_extension.BuildExtension})

在此代碼中,CppExtensionsetuptools.Extension 的一個方便的 wrapper,它傳遞正確的包含路徑并將擴展的語言設置為 C++。 等效的原 setuptools 代碼是:

Extension(name='lltm_cpp',sources=['lltm.cpp'],include_dirs=cpp_extension.include_paths(),language='c++')

BuildExtension 執行許多必需的配置步驟和檢查,并在混合 C++/CUDA 擴展的情況下管理混合編譯。 這就是我們現在真正需要了解的關于構建 C++ 擴展的全部內容! 現在讓我們看看我們的 C++ 擴展的實現,即 lltm.cpp

編寫C++算子

接下來我們開始用 C++ 實現 LLTM。反向傳播需要的一個函數是 sigmoid 的導數。 下面一小段代碼,我們據此來討論在編寫 C++ 擴展時的環境:

#include <torch/extension.h>
#include <iostream>torch::Tensor d_sigmoid(torch::Tensor z) {auto s = torch::sigmoid(z);return (1 - s) * s;
}

<torch/extension.h> 是 “一站式” 頭文件,包含編寫 C++ 擴展所需的所有 PyTorch 內容。 這包括:

  • ATen 庫,這是我們用于張量計算的主要 API,
  • pybind11,這是我們為 C++ 代碼創建 Python 綁定的方式,
  • 以及管理 ATen 和 pybind11 之間交互細節的頭文件。

d_sigmoid() 的實現展示了如何使用 ATen API。 PyTorch 的張量和變量接口是從 ATen 庫自動生成的,因此我們可以或多或少地將 Python 實現 1:1 轉換為 C++。 我們所有計算的主要數據類型是 torch::Tensor。 可以在這里查看其完整的 API。 另請注意,我們可以包含 <iostream> 等 C 或 C++ 頭文件,并支持 C++11 的全部功能。

請注意,在 Windows 上解析 torch/extension.h 時,CUDA-11.5 nvcc 會遇到內部編譯器錯誤。 要解決此問題,請將 python 綁定邏輯移動到純 C++ 文件。

示例如下,使用:

#include <ATen/ATen.h>
at::Tensor SigmoidAlphaBlendForwardCuda(....)

而不要:

#include <torch/extension.h>
torch::Tensor SigmoidAlphaBlendForwardCuda(...)

這時 nvcc 的一個bug,目前仍是 open 的 issue,完整解決的代碼示例在這里。

前向傳播

下面,我們給出完整前向傳播的 C++ 實現:

#include <vector>std::vector<at::Tensor> lltm_forward(torch::Tensor input,torch::Tensor weights,torch::Tensor bias,torch::Tensor old_h,torch::Tensor old_cell) {auto X = torch::cat({old_h, input}, /*dim=*/1);auto gate_weights = torch::addmm(bias, X, weights.transpose(0, 1));auto gates = gate_weights.chunk(3, /*dim=*/1);auto input_gate = torch::sigmoid(gates[0]);auto output_gate = torch::sigmoid(gates[1]);auto candidate_cell = torch::elu(gates[2], /*alpha=*/1.0);auto new_cell = old_cell + candidate_cell * input_gate;auto new_h = torch::tanh(new_cell) * output_gate;return {new_h,new_cell,input_gate,output_gate,candidate_cell,X,gate_weights};
}

反向傳播

C++ 擴展 API 目前沒有為我們提供自動生成反向傳播函數的方法(之前提到 Python 可以)。 因此,我們還必須自己實現 LLTM 的反向傳播,它計算損失關于前向傳播的每個輸入的導數。 最終,我們將前向和后向函數都放入 torch.autograd.Function 中,來創建一個的 Python binding。 反向函數稍微復雜一些,因此我們不會深入研究代碼(如果有興趣,可以閱讀 Alex Graves 的論文以獲取更多信息):

// tanh'(z) = 1 - tanh^2(z)
torch::Tensor d_tanh(torch::Tensor z) {return 1 - z.tanh().pow(2);
}// elu'(z) = relu'(z) + { alpha * exp(z) if (alpha * (exp(z) - 1)) < 0, else 0}
torch::Tensor d_elu(torch::Tensor z, torch::Scalar alpha = 1.0) {auto e = z.exp();auto mask = (alpha * (e - 1)) < 0;return (z > 0).type_as(z) + mask.type_as(z) * (alpha * e);
}std::vector<torch::Tensor> lltm_backward(torch::Tensor grad_h,torch::Tensor grad_cell,torch::Tensor new_cell,torch::Tensor input_gate,torch::Tensor output_gate,torch::Tensor candidate_cell,torch::Tensor X,torch::Tensor gate_weights,torch::Tensor weights) {auto d_output_gate = torch::tanh(new_cell) * grad_h;auto d_tanh_new_cell = output_gate * grad_h;auto d_new_cell = d_tanh(new_cell) * d_tanh_new_cell + grad_cell;auto d_old_cell = d_new_cell;auto d_candidate_cell = input_gate * d_new_cell;auto d_input_gate = candidate_cell * d_new_cell;auto gates = gate_weights.chunk(3, /*dim=*/1);d_input_gate *= d_sigmoid(gates[0]);d_output_gate *= d_sigmoid(gates[1]);d_candidate_cell *= d_elu(gates[2]);auto d_gates =torch::cat({d_input_gate, d_output_gate, d_candidate_cell}, /*dim=*/1);auto d_weights = d_gates.t().mm(X);auto d_bias = d_gates.sum(/*dim=*/0, /*keepdim=*/true);auto d_X = d_gates.mm(weights);const auto state_size = grad_h.size(1);auto d_old_h = d_X.slice(/*dim=*/1, 0, state_size);auto d_input = d_X.slice(/*dim=*/1, state_size);return {d_old_h, d_input, d_weights, d_bias, d_old_cell};
}

綁定到Python

使用 C++ 和 ATen 編寫算子后,我們使用 pybind11 將 C++ 函數或類綁定到 Python。 如果對這部分 PyTorch C++ 擴展的疑問或問題,請參考 pybind11 文檔解決。

對于我們的擴展,必要的綁定代碼只需要四行:

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {m.def("forward", &lltm_forward, "LLTM forward");m.def("backward", &lltm_backward, "LLTM backward");
}

這里需要注意的一點是宏 TORCH_EXTENSION_NAME。 torch 擴展構建將其定義為我們在 setup.py 腳本中為擴展提供的名稱。 在這種情況下,TORCH_EXTENSION_NAME 的值將是 “lltm_cpp”。 這是為了避免必須在兩個地方(構建腳本和我們的 C++ 代碼)維護擴展名,因為兩者之間的不匹配會導致很多麻煩的問題。

使用擴展

我們現在準備在 PyTorch 中導入擴展。 此時,目錄結構如下所示:

pytorch/lltm-extension/lltm.cppsetup.py

現在,運行 python setup.py install 來構建和安裝擴展。 輸出應該類似:

running install
running bdist_egg
running egg_info
creating lltm_cpp.egg-info
writing lltm_cpp.egg-info/PKG-INFO
writing dependency_links to lltm_cpp.egg-info/dependency_links.txt
writing top-level names to lltm_cpp.egg-info/top_level.txt
writing manifest file 'lltm_cpp.egg-info/SOURCES.txt'
reading manifest file 'lltm_cpp.egg-info/SOURCES.txt'
writing manifest file 'lltm_cpp.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-x86_64/egg
running install_lib
running build_ext
building 'lltm_cpp' extension
creating build
creating build/temp.linux-x86_64-3.7
gcc -pthread -B ~/local/miniconda/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I~/local/miniconda/lib/python3.7/site-packages/torch/include -I~/local/miniconda/lib/python3.7/site-packages/torch/include/torch/csrc/api/include -I~/local/miniconda/lib/python3.7/site-packages/torch/include/TH -I~/local/miniconda/lib/python3.7/site-packages/torch/include/THC -I~/local/miniconda/include/python3.7m -c lltm.cpp -o build/temp.linux-x86_64-3.7/lltm.o -DTORCH_API_INCLUDE_EXTENSION_H -DTORCH_EXTENSION_NAME=lltm_cpp -D_GLIBCXX_USE_CXX11_ABI=1 -std=c++11
cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++
creating build/lib.linux-x86_64-3.7
g++ -pthread -shared -B ~/local/miniconda/compiler_compat -L~/local/miniconda/lib -Wl,-rpath=~/local/miniconda/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.7/lltm.o -o build/lib.linux-x86_64-3.7/lltm_cpp.cpython-37m-x86_64-linux-gnu.so
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/egg
copying build/lib.linux-x86_64-3.7/lltm_cpp.cpython-37m-x86_64-linux-gnu.so -> build/bdist.linux-x86_64/egg
creating stub loader for lltm_cpp.cpython-37m-x86_64-linux-gnu.so
byte-compiling build/bdist.linux-x86_64/egg/lltm_cpp.py to lltm_cpp.cpython-37.pyc
creating build/bdist.linux-x86_64/egg/EGG-INFO
copying lltm_cpp.egg-info/PKG-INFO -> build/bdist.linux-x86_64/egg/EGG-INFO
copying lltm_cpp.egg-info/SOURCES.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying lltm_cpp.egg-info/dependency_links.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying lltm_cpp.egg-info/top_level.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
writing build/bdist.linux-x86_64/egg/EGG-INFO/native_libs.txt
zip_safe flag not set; analyzing archive contents...
__pycache__.lltm_cpp.cpython-37: module references __file__
creating 'dist/lltm_cpp-0.0.0-py3.7-linux-x86_64.egg' and adding 'build/bdist.linux-x86_64/egg' to it
removing 'build/bdist.linux-x86_64/egg' (and everything under it)
Processing lltm_cpp-0.0.0-py3.7-linux-x86_64.egg
removing '~/local/miniconda/lib/python3.7/site-packages/lltm_cpp-0.0.0-py3.7-linux-x86_64.egg' (and everything under it)
creating ~/local/miniconda/lib/python3.7/site-packages/lltm_cpp-0.0.0-py3.7-linux-x86_64.egg
Extracting lltm_cpp-0.0.0-py3.7-linux-x86_64.egg to ~/local/miniconda/lib/python3.7/site-packages
lltm-cpp 0.0.0 is already the active version in easy-install.pthInstalled ~/local/miniconda/lib/python3.7/site-packages/lltm_cpp-0.0.0-py3.7-linux-x86_64.egg
Processing dependencies for lltm-cpp==0.0.0
Finished processing dependencies for lltm-cpp==0.0.0

關于編譯器的一點說明:由于 ABI 版本控制問題,用于構建 C++ 擴展的編譯器必須與構建 PyTorch 的編譯器 ABI 兼容。 實際上,這意味著必須在 Linux 上使用 GCC 4.9 及更高版本。 對于 Ubuntu 16.04 和其他較新的 Linux 發行版,這應該已經是默認編譯器了。 在 MacOS 上,必須使用 clang(沒有任何 ABI 版本問題)。 最麻煩的情況,需要從源代碼構建 PyTorch,然后使用相同的編譯器構建擴展。

構建擴展后,我們可以使用在 setup.py 腳本中指定的名稱將其簡單地導入 Python。 請務必先導入 torch,因為這將解析動態鏈接器必須看到的一些符號:

In [1]: import torch
In [2]: import lltm_cpp
In [3]: lltm_cpp.forward
Out[3]: <function lltm.PyCapsule.forward>

如果我們在函數或模塊上調用 help(),我們可以看到它的簽名與我們的 C++ 代碼匹配:

In[4] help(lltm_cpp.forward)
forward(...) method of builtins.PyCapsule instanceforward(arg0: torch::Tensor, arg1: torch::Tensor, arg2: torch::Tensor, arg3: torch::Tensor, arg4: torch::Tensor) -> List[torch::Tensor]LLTM forward

由于我們現在可以從 Python 調用我們的 C++ 函數,我們可以用 torch.autograd.Functiontorch.nn.Module 包裝它們,使它們成為 PyTorch 的一等公民:

import math
import torch# Our module!
import lltm_cppclass LLTMFunction(torch.autograd.Function):@staticmethoddef forward(ctx, input, weights, bias, old_h, old_cell):outputs = lltm_cpp.forward(input, weights, bias, old_h, old_cell)new_h, new_cell = outputs[:2]variables = outputs[1:] + [weights]ctx.save_for_backward(*variables)return new_h, new_cell@staticmethoddef backward(ctx, grad_h, grad_cell):outputs = lltm_cpp.backward(grad_h.contiguous(), grad_cell.contiguous(), *ctx.saved_tensors)d_old_h, d_input, d_weights, d_bias, d_old_cell = outputsreturn d_input, d_weights, d_bias, d_old_h, d_old_cellclass LLTM(torch.nn.Module):def __init__(self, input_features, state_size):super(LLTM, self).__init__()self.input_features = input_featuresself.state_size = state_sizeself.weights = torch.nn.Parameter(torch.empty(3 * state_size, input_features + state_size))self.bias = torch.nn.Parameter(torch.empty(3 * state_size))self.reset_parameters()def reset_parameters(self):stdv = 1.0 / math.sqrt(self.state_size)for weight in self.parameters():weight.data.uniform_(-stdv, +stdv)def forward(self, input, state):return LLTMFunction.apply(input, self.weights, self.bias, *state)

性能對比

現在我們可以從 PyTorch 使用和調用我們的 C++ 代碼,我們可以運行一個小型基準測試,看看我們通過用 C++ 重寫我們的操作獲得了多少性能。 我們將前向和反向運行 LLTM 幾次并測量持續時間:

import timeimport torchbatch_size = 16
input_features = 32
state_size = 128X = torch.randn(batch_size, input_features)
h = torch.randn(batch_size, state_size)
C = torch.randn(batch_size, state_size)rnn = LLTM(input_features, state_size)forward = 0
backward = 0
for _ in range(100000):start = time.time()new_h, new_C = rnn(X, (h, C))forward += time.time() - startstart = time.time()(new_h.sum() + new_C.sum()).backward()backward += time.time() - startprint('Forward: {:.3f} s | Backward {:.3f} s'.format(forward, backward))

如果我們使用本文開頭用純 Python 編寫的原始 LLTM 運行此代碼,我們會得到以下數字(在我的機器上):

Forward: 506.480 us | Backward 444.694 us

以及我們新的 C++ 版本:

Forward: 349.335 us | Backward 443.523 us

GPU上的性能

關于 PyTorch 的 ATen 后端的一個奇妙的事實是它抽象了我們正在運行的計算設備。 這意味著我們為 CPU 編寫的相同代碼也可以在 GPU 上運行,并且各個操作將相應地分派給 GPU 優化的實現。 對于矩陣乘法(如 mmaddmm)等某些操作,這是一個巨大的勝利。 讓我們看看使用 CUDA 張量運行 C++ 代碼可以獲得多少性能。 不需要對我們的實現進行任何更改,我們只需要將我們的張量從 Python 放入 GPU 內存中,在創建時添加 device=cuda_device 參數或在創建后使用 .to(cuda_device)

import torchassert torch.cuda.is_available()
cuda_device = torch.device("cuda")  # device object representing GPUbatch_size = 16
input_features = 32
state_size = 128# Note the device=cuda_device arguments here
X = torch.randn(batch_size, input_features, device=cuda_device)
h = torch.randn(batch_size, state_size, device=cuda_device)
C = torch.randn(batch_size, state_size, device=cuda_device)rnn = LLTM(input_features, state_size).to(cuda_device)forward = 0
backward = 0
for _ in range(100000):start = time.time()new_h, new_C = rnn(X, (h, C))torch.cuda.synchronize()forward += time.time() - startstart = time.time()(new_h.sum() + new_C.sum()).backward()torch.cuda.synchronize()backward += time.time() - startprint('Forward: {:.3f} us | Backward {:.3f} us'.format(forward * 1e6/1e5, backward * 1e6/1e5))

再次將我們的普通 PyTorch 代碼與現在都在 CUDA 設備上運行的 C++ 版本進行比較,我們再次看到了性能提升。 對于 Python/PyTorch:

Forward: 187.719 us | Backward 410.815 us

使用 C++/ATen:

Forward: 149.802 us | Backward 393.458 us

與非 CUDA 代碼相比,整體上有一個不錯的加速。 但是,我們可以通過編寫自定義 CUDA 內核來從 C++ 代碼中獲得更多性能,我們將很快深入探討。 在此之前,讓我們討論另一種構建 C++ 擴展的方法。

JIT編譯擴展

之前,我提到有兩種構建 C++ 擴展的方法:使用 setuptools 或 just in time (JIT)。 講完了前者,我們再來談談后者。 JIT 編譯機制通過調用 PyTorch API 中名為 torch.utils.cpp_extension.load() 的函數,為我們提供了一種即時編譯和加載擴展的方法。 對于 LLTM:

from torch.utils.cpp_extension import loadlltm_cpp = load(name="lltm_cpp", sources=["lltm.cpp"])

在這里,我們為該函數提供與 setuptools 相同的信息。 在后臺,這將執行以下操作:

  1. 創建一個臨時目錄/tmp/torch_extensions/lltm,
  2. 將 Ninja 構建文件發送到該臨時目錄中,
  3. 將源文件編譯到共享庫中,
  4. 將此共享庫作為 Python 模塊導入。

如果想看到這個詳細的過程,可以給 cpp_extension.load() 傳遞參數 verbose=True

Using /tmp/torch_extensions as PyTorch extensions root...
Emitting ninja build file /tmp/torch_extensions/lltm_cpp/build.ninja...
Building extension module lltm_cpp...
Loading extension module lltm_cpp...

生成的 Python 模塊將與 setuptools 生成的完全相同,但無需維護單獨的 setup.py 構建文件。 如果我們的設置更復雜,并且確實需要 setuptools 的全部功能,我們可以編寫自己的 setup.py 但在許多情況下,這種 JIT 技術就可以了。 第一次運行這一行時,需要一些時間,因為擴展程序正在后臺編譯。 由于我們使用 Ninja 構建系統來構建源代碼,因此重新編譯是增量的,因此當第二次運行 Python 模塊時重新加載擴展程序很快,并且如果不更改擴展程序的源文件,開銷也很低。

編寫C++/CUDA混合擴展

為了真正將我們的實現提升到一個新的水平,我們可以使用自定義 CUDA 核手動編寫前向和反向傳播的部分內容。對于 LLTM,這具有會特別有效,因為順序中有大量的逐點操作,它們都可以在單個 CUDA 核中融合和并行化。讓我們看看我們如何編寫這樣的 CUDA 核并使用這種擴展機制將其與 PyTorch 集成。

編寫 CUDA 擴展的一般策略是首先編寫一個 C++ 文件,該文件定義將從 Python 調用的函數,并使用 pybind11 將這些函數綁定到 Python。此外,該文件還將聲明在 CUDA (.cu) 文件中定義的函數。然后,C++ 函數將進行一些檢查并最終將其調用轉發給 CUDA 函數。在 CUDA 文件中,我們編寫了實際的 CUDA 核。然后,cpp_extension 包將負責使用 gcc 等 C++ 編譯器編譯 C++ 源代碼,并使用 NVIDIA 的 nvcc 編譯器編譯 CUDA 源代碼。這確保了每個編譯器都處理它最了解的編譯文件。最終,它們將被鏈接到一個共享庫中,我們可以從 Python 代碼中使用它。

我們將從 C++ 文件開始,我們將其稱為 lltm_cuda.cpp,如下:

#include <torch/extension.h>#include <vector>// CUDA forward declarationsstd::vector<torch::Tensor> lltm_cuda_forward(torch::Tensor input,torch::Tensor weights,torch::Tensor bias,torch::Tensor old_h,torch::Tensor old_cell);std::vector<torch::Tensor> lltm_cuda_backward(torch::Tensor grad_h,torch::Tensor grad_cell,torch::Tensor new_cell,torch::Tensor input_gate,torch::Tensor output_gate,torch::Tensor candidate_cell,torch::Tensor X,torch::Tensor gate_weights,torch::Tensor weights);// C++ interface#define CHECK_CUDA(x) TORCH_CHECK(x.device().is_cuda(), #x " must be a CUDA tensor")
#define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)std::vector<torch::Tensor> lltm_forward(torch::Tensor input,torch::Tensor weights,torch::Tensor bias,torch::Tensor old_h,torch::Tensor old_cell) {CHECK_INPUT(input);CHECK_INPUT(weights);CHECK_INPUT(bias);CHECK_INPUT(old_h);CHECK_INPUT(old_cell);return lltm_cuda_forward(input, weights, bias, old_h, old_cell);
}std::vector<torch::Tensor> lltm_backward(torch::Tensor grad_h,torch::Tensor grad_cell,torch::Tensor new_cell,torch::Tensor input_gate,torch::Tensor output_gate,torch::Tensor candidate_cell,torch::Tensor X,torch::Tensor gate_weights,torch::Tensor weights) {CHECK_INPUT(grad_h);CHECK_INPUT(grad_cell);CHECK_INPUT(input_gate);CHECK_INPUT(output_gate);CHECK_INPUT(candidate_cell);CHECK_INPUT(X);CHECK_INPUT(gate_weights);CHECK_INPUT(weights);return lltm_cuda_backward(grad_h,grad_cell,new_cell,input_gate,output_gate,candidate_cell,X,gate_weights,weights);
}PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {m.def("forward", &lltm_forward, "LLTM forward (CUDA)");m.def("backward", &lltm_backward, "LLTM backward (CUDA)");
}

如我們所見,它主要是樣板,檢查和轉發到我們將在 CUDA 文件中定義的函數。 我們將此文件命名為 lltm_cuda_kernel.cu(注意 .cu 擴展名!)。 NVCC 可以編譯 C++11,因此我們仍然可以使用 ATen 和 C++ 標準庫(但不是 torch.h)。 請注意,setuptools 無法處理具有相同名稱但擴展名不同的文件,因此如果使用 setup.py 方法而不是 JIT 方法,則必須為 CUDA 文件指定一個與 C++ 文件不同的名稱(對于 JIT 方法,lltm. cpp 和 lltm.cu 可以正常工作)。 讓我們看一下這個文件的樣子:

#include <torch/extension.h>#include <cuda.h>
#include <cuda_runtime.h>#include <vector>template <typename scalar_t>
__device__ __forceinline__ scalar_t sigmoid(scalar_t z) {return 1.0 / (1.0 + exp(-z));
}

在這里,我們看到了我剛剛描述的頭文件,以及我們使用 CUDA 特定語法(如 __device____forceinline__ )和函數(如 exp)。 我們繼續使用一些輔助函數:

template <typename scalar_t>
__device__ __forceinline__ scalar_t d_sigmoid(scalar_t z) {const auto s = sigmoid(z);return (1.0 - s) * s;
}template <typename scalar_t>
__device__ __forceinline__ scalar_t d_tanh(scalar_t z) {const auto t = tanh(z);return 1 - (t * t);
}template <typename scalar_t>
__device__ __forceinline__ scalar_t elu(scalar_t z, scalar_t alpha = 1.0) {return fmax(0.0, z) + fmin(0.0, alpha * (exp(z) - 1.0));
}template <typename scalar_t>
__device__ __forceinline__ scalar_t d_elu(scalar_t z, scalar_t alpha = 1.0) {const auto e = exp(z);const auto d_relu = z < 0.0 ? 0.0 : 1.0;return d_relu + (((alpha * (e - 1.0)) < 0.0) ? (alpha * e) : 0.0);
}

現在要實際實現一個函數,我們還需要兩件事:一個執行我們不希望顯式手動編寫的操作并調用 CUDA 內核的函數,然后是我們想要加速的部分的實際 CUDA 內核 . 對于前向傳遞,第一個函數應該如下所示:

std::vector<torch::Tensor> lltm_cuda_forward(torch::Tensor input,torch::Tensor weights,torch::Tensor bias,torch::Tensor old_h,torch::Tensor old_cell) {auto X = torch::cat({old_h, input}, /*dim=*/1);auto gates = torch::addmm(bias, X, weights.transpose(0, 1));const auto batch_size = old_cell.size(0);const auto state_size = old_cell.size(1);auto new_h = torch::zeros_like(old_cell);auto new_cell = torch::zeros_like(old_cell);auto input_gate = torch::zeros_like(old_cell);auto output_gate = torch::zeros_like(old_cell);auto candidate_cell = torch::zeros_like(old_cell);const int threads = 1024;const dim3 blocks((state_size + threads - 1) / threads, batch_size);AT_DISPATCH_FLOATING_TYPES(gates.type(), "lltm_forward_cuda", ([&] {lltm_cuda_forward_kernel<scalar_t><<<blocks, threads>>>(gates.data<scalar_t>(),old_cell.data<scalar_t>(),new_h.data<scalar_t>(),new_cell.data<scalar_t>(),input_gate.data<scalar_t>(),output_gate.data<scalar_t>(),candidate_cell.data<scalar_t>(),state_size);}));return {new_h, new_cell, input_gate, output_gate, candidate_cell, X, gates};
}

這里的關鍵是 AT_DISPATCH_FLOATING_TYPES 宏和內核啟動(由 <<<...>>> 指示)。 雖然 ATen 抽象出我們處理的張量的設備和數據類型,但張量在運行時仍將由具體設備上的具體類型的內存支持。 因此,我們需要一種在運行時確定張量是什么類型的方法,然后選擇性地調用具有相應正確類型簽名的函數。 手動完成,如下:

switch (tensor.type().scalarType()) {case torch::ScalarType::Double:return function<double>(tensor.data<double>());case torch::ScalarType::Float:return function<float>(tensor.data<float>());...
}

AT_DISPATCH_FLOATING_TYPES 的目的是為我們處理這個調度。它需要一個類型(在我們的例子中是 gates.type())、一個名稱(用于顯示錯誤消息)和一個 lambda 函數。在這個 lambda 函數中,類型別名 scalar_t 可用,并被定義為張量在運行時在該 context 中實際存在的類型。因此,如果我們有一個模板函數(就是我們的 CUDA 核函數),我們可以用這個 scalar_t 別名實例化它,然后調用正確的函數。在這種情況下,我們還希望檢索張量的數據指針作為該 scalar_t 類型的指針。如果想分派所有類型而不僅僅是浮點類型(Float 和 Double),可以使用 AT_DISPATCH_ALL_TYPES

請注意,我們使用純 ATen 執行一些操作。這些操作仍將在 GPU 上運行,但使用 ATen 的默認實現。這是有道理的,因為 ATen 將使用高度優化的例程來處理矩陣乘法(例如 addmm)或卷積,這部分很難實現和改進。

至于內核啟動本身,我們在此指定每個 CUDA block 有 1024 個線程,并且整個 GPU 網格被分成多個 1 x 1024 線程塊,以每個組件一個線程填充我們的矩陣。例如,如果我們的 state_size 大小為 2048,batch_size 大小為 4,我們將啟動總共 4 x 2 = 8 個塊,每個 1024 個線程。如果不了解 CUDA“block”或“grid”,那么可以參考閱讀有關 CUDA 的介紹性讀物。

實際的 CUDA 核相當簡單(如果有 GPU 編程的經驗):

template <typename scalar_t>
__global__ void lltm_cuda_forward_kernel(const scalar_t* __restrict__ gates,const scalar_t* __restrict__ old_cell,scalar_t* __restrict__ new_h,scalar_t* __restrict__ new_cell,scalar_t* __restrict__ input_gate,scalar_t* __restrict__ output_gate,scalar_t* __restrict__ candidate_cell,size_t state_size) {const int column = blockIdx.x * blockDim.x + threadIdx.x;const int index = blockIdx.y * state_size + column;const int gates_row = blockIdx.y * (state_size * 3);if (column < state_size) {input_gate[index] = sigmoid(gates[gates_row + column]);output_gate[index] = sigmoid(gates[gates_row + state_size + column]);candidate_cell[index] = elu(gates[gates_row + 2 * state_size + column]);new_cell[index] =old_cell[index] + candidate_cell[index] * input_gate[index];new_h[index] = tanh(new_cell[index]) * output_gate[index];}
}

有趣的是,我們能夠為門矩陣中的每個單獨組件完全并行計算所有這些逐點操作。 想象以下如果用一個巨大的 for 循環超過一百萬個串行元素來做到這一點,就會明白為什么這會快得多。

使用accessors

可以在 CUDA 核中看到我們直接處理對應類型的指針。 事實上,直接在 cuda 核中使用高級類型不可知的張量是非常低效的。然而,這是以易用性和可讀性為代價的,尤其是對于高維數據。 在我們的示例中,我們知道例如連續門張量具有 3 個維度:

  • batch,大小為 batch_size,步長為 3*state_size
  • row,大小為 3,步長為 state_size
  • index,大小為 state_size ,步長為 1

那么我們如何訪問內核中的元素 gates[n][row][column] 呢? 事實證明,可以通過一些簡單的算術來訪問對應位置的元素。

gates.data<scalar_t>()[n*3*state_size + row*state_size + column]

這個表達式需要明確知道步長,并在其參數中傳遞給核函數。 可以看到,在核函數接受多個不同大小的張量的情況下,最終會得到一個很長的參數列表。對我們來說幸運的是,ATen 提供了通過單一動態檢查創建的訪問器,張量是維度的類型和數量。 然后訪問器公開一個 API 以有效地訪問張量元素,而無需轉換為單個指針:

torch::Tensor foo = torch::rand({12, 12});// assert foo is 2-dimensional and holds floats.
auto foo_a = foo.accessor<float,2>();
float trace = 0;for(int i = 0; i < foo_a.size(0); i++) {// use the accessor foo_a to get tensor data.trace += foo_a[i][i];
}

Accessor 對象有一個相對高層的接口,具有 .size().stride() 方法以及多維索引。 .accessor<> 接口旨在有效地訪問 cpu 張量上的數據。 對應于 cuda 張量的是 packed_accessor64<>packed_accessor32<>,它們生成具有 64 位或 32 位整數索引的打包 accessor。

與 Accessor 的根本區別在于 Packed Accessor 將大小和步長數據復制到其結構內部,而不是指向它。 它允許我們將其傳遞給 CUDA 內核函數并在其中使用其接口。

我們可以設計一個使用 Packed Accessors 而不是指針的函數。

__global__ void lltm_cuda_forward_kernel(const torch::PackedTensorAccessor32<scalar_t,3,torch::RestrictPtrTraits> gates,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> old_cell,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> new_h,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> new_cell,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> input_gate,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> output_gate,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> candidate_cell)

讓我們分解這里使用的模板。 前兩個參數 scalar_t 和 2 與常規 accessor 相同。 參數 torch::RestrictPtrTraits 表示必須使用 __restrict__ 關鍵字。 另請注意,我們使用了 PackedAccessor32 變體,它將大小和步幅存儲在 int32_t 中。 這很重要,因為使用 64 位變體 (PackedAccessor64) 會使核變慢。

函數聲明變為:

template <typename scalar_t>
__global__ void lltm_cuda_forward_kernel(const torch::PackedTensorAccessor32<scalar_t,3,torch::RestrictPtrTraits> gates,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> old_cell,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> new_h,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> new_cell,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> input_gate,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> output_gate,torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> candidate_cell) {//batch indexconst int n = blockIdx.y;// column indexconst int c = blockIdx.x * blockDim.x + threadIdx.x;if (c < gates.size(2)){input_gate[n][c] = sigmoid(gates[n][0][c]);output_gate[n][c] = sigmoid(gates[n][1][c]);candidate_cell[n][c] = elu(gates[n][2][c]);new_cell[n][c] =old_cell[n][c] + candidate_cell[n][c] * input_gate[n][c];new_h[n][c] = tanh(new_cell[n][c]) * output_gate[n][c];}
}

該實現更具可讀性。然后通過在 host 函數中使用 .packed_accessor32<> 方法創建打包訪問器來調用此函數。

std::vector<torch::Tensor> lltm_cuda_forward(torch::Tensor input,torch::Tensor weights,torch::Tensor bias,torch::Tensor old_h,torch::Tensor old_cell) {auto X = torch::cat({old_h, input}, /*dim=*/1);auto gate_weights = torch::addmm(bias, X, weights.transpose(0, 1));const auto batch_size = old_cell.size(0);const auto state_size = old_cell.size(1);auto gates = gate_weights.reshape({batch_size, 3, state_size});auto new_h = torch::zeros_like(old_cell);auto new_cell = torch::zeros_like(old_cell);auto input_gate = torch::zeros_like(old_cell);auto output_gate = torch::zeros_like(old_cell);auto candidate_cell = torch::zeros_like(old_cell);const int threads = 1024;const dim3 blocks((state_size + threads - 1) / threads, batch_size);AT_DISPATCH_FLOATING_TYPES(gates.type(), "lltm_forward_cuda", ([&] {lltm_cuda_forward_kernel<scalar_t><<<blocks, threads>>>(gates.packed_accessor32<scalar_t,3,torch::RestrictPtrTraits>(),old_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),new_h.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),new_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),input_gate.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),output_gate.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),candidate_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>());}));return {new_h, new_cell, input_gate, output_gate, candidate_cell, X, gates};
}

反向傳播遵循類似,不再贅述:

template <typename scalar_t>
__global__ void lltm_cuda_backward_kernel(torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> d_old_cell,torch::PackedTensorAccessor32<scalar_t,3,torch::RestrictPtrTraits> d_gates,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> grad_h,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> grad_cell,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> new_cell,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> input_gate,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> output_gate,const torch::PackedTensorAccessor32<scalar_t,2,torch::RestrictPtrTraits> candidate_cell,const torch::PackedTensorAccessor32<scalar_t,3,torch::RestrictPtrTraits> gate_weights) {//batch indexconst int n = blockIdx.y;// column indexconst int c = blockIdx.x * blockDim.x + threadIdx.x;if (c < d_gates.size(2)){const auto d_output_gate = tanh(new_cell[n][c]) * grad_h[n][c];const auto d_tanh_new_cell = output_gate[n][c] * grad_h[n][c];const auto d_new_cell =d_tanh(new_cell[n][c]) * d_tanh_new_cell + grad_cell[n][c];d_old_cell[n][c] = d_new_cell;const auto d_candidate_cell = input_gate[n][c] * d_new_cell;const auto d_input_gate = candidate_cell[n][c] * d_new_cell;d_gates[n][0][c] =d_input_gate * d_sigmoid(gate_weights[n][0][c]);d_gates[n][1][c] =d_output_gate * d_sigmoid(gate_weights[n][1][c]);d_gates[n][2][c] =d_candidate_cell * d_elu(gate_weights[n][2][c]);}
}std::vector<torch::Tensor> lltm_cuda_backward(torch::Tensor grad_h,torch::Tensor grad_cell,torch::Tensor new_cell,torch::Tensor input_gate,torch::Tensor output_gate,torch::Tensor candidate_cell,torch::Tensor X,torch::Tensor gates,torch::Tensor weights) {auto d_old_cell = torch::zeros_like(new_cell);auto d_gates = torch::zeros_like(gates);const auto batch_size = new_cell.size(0);const auto state_size = new_cell.size(1);const int threads = 1024;const dim3 blocks((state_size + threads - 1) / threads, batch_size);AT_DISPATCH_FLOATING_TYPES(X.type(), "lltm_backward_cuda", ([&] {lltm_cuda_backward_kernel<scalar_t><<<blocks, threads>>>(d_old_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),d_gates.packed_accessor32<scalar_t,3,torch::RestrictPtrTraits>(),grad_h.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),grad_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),new_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),input_gate.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),output_gate.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),candidate_cell.packed_accessor32<scalar_t,2,torch::RestrictPtrTraits>(),gates.packed_accessor32<scalar_t,3,torch::RestrictPtrTraits>());}));auto d_gate_weights = d_gates.reshape({batch_size, 3*state_size});auto d_weights = d_gate_weights.t().mm(X);auto d_bias = d_gate_weights.sum(/*dim=*/0, /*keepdim=*/true);auto d_X = d_gate_weights.mm(weights);auto d_old_h = d_X.slice(/*dim=*/1, 0, state_size);auto d_input = d_X.slice(/*dim=*/1, state_size);return {d_old_h, d_input, d_weights, d_bias, d_old_cell, d_gates};
}

將C++/CUDA算子整合到PyTorch中

我們支持 CUDA 的操作與 PyTorch 的集成就很簡單了。 如果用 setup.py 腳本,如下所示:

from setuptools import setup
from torch.utils.cpp_extension import BuildExtension, CUDAExtensionsetup(name='lltm',ext_modules=[CUDAExtension('lltm_cuda', ['lltm_cuda.cpp','lltm_cuda_kernel.cu',])],cmdclass={'build_ext': BuildExtension})

我們現在使用 CUDAExtension() 代替 CppExtension()。 我們可以只指定 .cu 文件和 .cpp 文件——該庫會為我們解決所有這些麻煩。 JIT 機制則更簡單:

from torch.utils.cpp_extension import loadlltm = load(name='lltm', sources=['lltm_cuda.cpp', 'lltm_cuda_kernel.cu'])

性能對比

我們希望將代碼的逐點操作與 CUDA 并行化和融合可以提高 LLTM 的性能。 讓我們看看這是否成立。 我們可以運行我之前列出的代碼來運行基準測試。 我們之前最快的版本是基于 CUDA 的 C++ 代碼:

Forward: 149.802 us | Backward 393.458 us

而如果使用我們自定義的 CUDA 核函數:

Forward: 129.431 us | Backward 304.641 us

更快了!

總結

大家現在應該對 PyTorch 的 C++ 擴展機制以及使用它們的動機有了很好的了解。 可以在這里找到本文中展示的代碼示例。 如果有任何問題,請使用論壇,或者看常見問題的 FAQ。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/532454.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/532454.shtml
英文地址,請注明出處:http://en.pswp.cn/news/532454.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

惠普800g1支持什么內存_惠普黑白激光打印機哪種好 惠普黑白激光打印機推薦【圖文詳解】...

打印機的出現讓我們在生活和日常工作中變得越來越方便&#xff0c;不過隨著科技的發展&#xff0c;打印機的類型也變得非常多&#xff0c;其中就有黑白激光打印機&#xff0c;而黑白激光打印機的品牌也有很多&#xff0c;比如我們的惠普黑白激光打印機&#xff0c;今天小編就給…

控制臺輸出顏色控制

控制臺輸出顏色控制 轉自&#xff1a;https://cloud.tencent.com/developer/article/1142372 前端時間&#xff0c;寫了一篇 PHP 在 Console 模式下的進度顯示 &#xff0c;正好最近的一個數據合并項目需要用到控制臺顏色輸出&#xff0c;所以就把相關的信息整理下&#xff0c;…

idea連接跳板機_跳板機服務(jumpserver)

一、跳板機服務作用介紹1、有效管理用戶權限信息2、有效記錄用戶登錄情況3、有效記錄用戶操作行為二、跳板機服務架構原理三、跳板機服務安裝過程第一步&#xff1a;安裝跳板機依賴軟件yum -y install git python-pip mariadb-devel gcc automake autoconf python-devel readl…

【詳細圖解】再次理解im2col

【詳細圖解】再次理解im2col 轉自&#xff1a;https://mp.weixin.qq.com/s/GPDYKQlIOq6Su0Ta9ipzig 一句話&#xff1a;im2col是將一個[C,H,W]矩陣變成一個[H,W]矩陣的一個方法&#xff0c;其原理是利用了行列式進行等價轉換。 為什么要做im2col? 減少調用gemm的次數。 重要…

反思 大班 快樂的機器人_幼兒園大班教案《快樂的桌椅》含反思

大班教案《快樂的桌椅》含反思適用于大班的體育主題教學活動當中&#xff0c;讓幼兒提高協調性和靈敏性&#xff0c;創新桌椅的玩法&#xff0c;正確爬的方法&#xff0c;學會匍匐前進&#xff0c;快來看看幼兒園大班《快樂的桌椅》含反思教案吧。幼兒園大班教案《快樂的桌椅》…

DCN可形變卷積實現1:Python實現

DCN可形變卷積實現1&#xff1a;Python實現 我們會先用純 Python 實現一個 Pytorch 版本的 DCN &#xff0c;然后實現其 C/CUDA 版本。 本文主要關注 DCN 可形變卷積的代碼實現&#xff0c;不會過多的介紹其思想&#xff0c;如有興趣&#xff0c;請參考論文原文&#xff1a; …

藍牙耳機聲音一頓一頓的_線控耳機黨陣地轉移成功,OPPO這款TWS耳機體驗滿分...

“你看到我手機里3.5mm的耳機孔了嗎”&#xff0c;這可能是許多線控耳機黨最想說的話了。確實&#xff0c;如今手機在做“減法”&#xff0c;而廠商們首先就拿3.5mm耳機孔“開刀”&#xff0c;我們也喪失了半夜邊充電邊戴耳機打游戲的樂趣。竟然如此&#xff0c;那如何在耳機、…

AI移動端優化之Im2Col+Pack+Sgemm

AI移動端優化之Im2ColPackSgemm 轉自&#xff1a;https://blog.csdn.net/just_sort/article/details/108412760 這篇文章是基于NCNN的Sgemm卷積為大家介紹Im2ColPackSgemm的原理以及算法實現&#xff0c;希望對算法優化感興趣或者做深度學習模型部署的讀者帶來幫助。 1. 前言 …

elementui的upload組件怎么獲取上傳的文本流、_抖音feed流直播間引流你還不會玩?實操講解...

本文由艾奇在線明星優化師寫作計劃出品在這個全民驚恐多災多難且帶有魔幻的2020&#xff0c;一場突如其來的疫情改變了人們很多消費習慣&#xff0c;同時加速了直播電商的發展&#xff0c;現在直播已經成為商家必爭的營銷之地&#xff0c;直播雖然很火&#xff0c;但如果沒有流…

FFmpeg 視頻處理入門教程

FFmpeg 視頻處理入門教程 轉自&#xff1a;https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html 作者&#xff1a; 阮一峰 日期&#xff1a; 2020年1月14日 FFmpeg 是視頻處理最常用的開源軟件。 它功能強大&#xff0c;用途廣泛&#xff0c;大量用于視頻網站和商業軟件&…

checkbox wpf 改變框的大小_【論文閱讀】傾斜目標范圍框(標注)的終極方案

前言最常用的斜框標注方式是在正框的基礎上加一個旋轉角度θ&#xff0c;其代數表示為(x_c,y_c,w,h,θ)&#xff0c;其中(x_c,y_c )表示范圍框中心點坐標&#xff0c;(w,h)表示范圍框的寬和高[1,2,7]。對于該標注方式&#xff0c;如果將w和h的值互換&#xff0c;再將θ加上或者…

徹底理解BP之手寫BP圖像分類你也行

徹底理解BP之手寫BP圖像分類你也行 轉自&#xff1a;https://zhuanlan.zhihu.com/p/397963213 第一節&#xff1a;用矩陣的視角&#xff0c;看懂BP的網絡圖 1.1、什么是BP反向傳播算法 BP(Back Propagation)誤差反向傳播算法&#xff0c;使用反向傳播算法的多層感知器又稱為B…

h5頁面禁止復制_H5移動端頁面禁止復制技巧

前言&#xff1a;業務需要&#xff0c;需要對整個頁面禁止彈出復制菜單。在禁止的頁面中加入以下css樣式定義* {-webkit-touch-callout:none;/*系統默認菜單被禁用*/-webkit-user-select:none;/*webkit瀏覽器*/-khtml-user-select:none;/*早起瀏覽器*/-moz-user-select:none;/*…

梯度下降法和牛頓法計算開根號

梯度下降法和牛頓法計算開根號 本文將介紹如何不調包&#xff0c;只能使用加減乘除法實現對根號x的求解。主要介紹梯度下降和牛頓法者兩種方法&#xff0c;并給出 C 實現。 梯度下降法 思路/步驟 轉化問題&#xff0c;將 x\sqrt{x}x? 的求解轉化為最小化目標函數&#xff…

匯博工業機器人碼垛機怎么寫_全自動碼垛機器人在企業生產中的地位越來越重要...

全自動碼垛機器人在企業生產中的地位越來越重要在智能化的各種全自動生產線中&#xff0c;全自動碼垛機器人成了全自動生產線的重要機械設備&#xff0c;在各種生產中發揮著不可忽視的作用。全自動碼垛機器人主要用于生產線上的包裝過程中&#xff0c;不僅能夠提高企業的生產率…

kmeans手寫實現與sklearn接口

kmeans手寫實現與sklearn接口 kmeans簡介 K 均值聚類是最基礎的一種聚類方法。它是一種迭代求解的聚類分析算法。 kmeans的迭代步驟 給各個簇中心 μ1,…,μc\mu_1,\dots,\mu_cμ1?,…,μc? 以適當的初值&#xff1b; 更新樣本 x1,…,xnx_1,\dots,x_nx1?,…,xn? 對應的…

小說中場景的功能_《流浪地球》:從小說到電影

2019年春節賀歲檔冒出一匹黑馬&#xff1a;國產科幻片《流浪地球》大年初一上映后口碑、票房雙豐收&#xff1a;截至9日下午&#xff0c;票房已破15億&#xff0c;并獲得9.2的高評分。著名導演詹姆斯卡梅隆通過社交媒體對我國春節期間上映的科幻影片《流浪地球》發出的祝愿&…

線性回歸與邏輯回歸及其實現

線性回歸與邏輯回歸及其實現 回歸與分類 預測值定性分析&#xff0c;即離散變量預測時&#xff0c;稱之為分類&#xff1b;預測值定量分析&#xff0c;即連續變量預測時&#xff0c;稱之為回歸。 如預測一張圖片是貓還是狗&#xff0c;是分類問題&#xff1b;預測明年的房價…

hbase 頁面訪問_HBase

HBase 特點 海量存儲 Hbase 適合存儲 PB 級別的海量數據&#xff0c;在 PB 級別的數據以及采用廉價 PC 存儲的情況下&#xff0c;能在幾十到百毫秒內返回數據。這與 Hbase 的極易擴展性息息相關。正式因為 Hbase 良好的擴展性&#xff0c;才為海量數據的存儲提供了便利。 2&…

深入理解L1、L2正則化

深入理解L1、L2正則化 轉自&#xff1a;【面試看這篇就夠了】L1、L2正則化理解 一、概述 正則化&#xff08;Regularization&#xff09;是機器學習中一種常用的技術&#xff0c;其主要目的是控制模型復雜度&#xff0c;減小過擬合。正則化技術已經成為模型訓練中的常用技術&a…