C++ OP相关注意事项
C++ OP相關(guān)注意事項(xiàng)
Paddle中Op的構(gòu)建邏輯
1.Paddle中Op的構(gòu)建邏輯
Paddle中所有的Op都繼承自O(shè)peratorBase,且所有的Op都是無(wú)狀態(tài)的,每個(gè)Op包含的成員變量只有四個(gè):type、inputs、outputs、attribute。
Op的核心方法是Run,Run方法需要兩方面的資源:數(shù)據(jù)資源和計(jì)算資源,這兩個(gè)資源分別通過(guò)Scope和Place獲取。框架內(nèi)部有一個(gè)全局的DeviceContextPool,用來(lái)記錄Place和DeviceContext之間的對(duì)應(yīng)的關(guān)系,即每個(gè)Place有且僅有一個(gè)DeviceContext與之對(duì)應(yīng),DeviceContext中存放了當(dāng)前設(shè)備的計(jì)算資源。比如對(duì)于GPU,這些資源包括cudnn_handle、cublas_handle、stream等,Op內(nèi)部所有的計(jì)算(數(shù)據(jù)拷貝和CUDA Kernel等)都必須在DeviceContext中進(jìn)行。
Paddle框架的設(shè)計(jì)理念是可以在多種設(shè)備及第三方庫(kù)上運(yùn)行,有些Op的實(shí)現(xiàn)可能會(huì)因?yàn)樵O(shè)備或者第三方庫(kù)的不同而不同。為此,Paddle引入了OpKernel的方式,即一個(gè)Op可以有多個(gè)OpKernel,這類Op繼承自O(shè)peratorWithKernel,這類Op的代表是conv_op,conv_op的OpKernel有:GemmConvKernel、CUDNNConvOpKernel、ConvMKLDNNOpKernel,且每個(gè)OpKernel都有double和float兩種數(shù)據(jù)類型。不需要OpKernel的代表有WhileOp等。
Operator繼承關(guān)系圖:
進(jìn)一步了解可參考:multi_devices,scope,Developer’s_Guide_to_Paddle_Fluid
2.Op的注冊(cè)邏輯
每個(gè)Operator的注冊(cè)項(xiàng)包括: C++ OpCreator creator_; GradOpMakerFN grad_op_maker_; proto::OpProto* proto_{nullptr}; OpAttrChecker* checker_{nullptr}; InferVarTypeFN infer_var_type_; InferShapeFN infer_shape_;
通常Op注釋時(shí)需要調(diào)用REGISTER_OPERATOR,即: REGISTER_OPERATOR(op_type, OperatorBase op_maker_and_checker_maker, op_grad_opmaker, op_infer_var_shape, op_infer_var_type)
注意:
- 對(duì)于所有Op,前三個(gè)參數(shù)是必須的,op_type指明op的名字,OperatorBase是該Op的對(duì)象,op_maker_and_checker_maker是op的maker以及Op中attr的checker。
- 如果該Op有反向,則必須要有op_grad_opmaker,因?yàn)樵赽ackward會(huì)根據(jù)正向的Op中獲取反向Op的Maker。
- 框架提供了一個(gè)默認(rèn)的op_grad_opmaker:DefaultGradOpDescMaker,這個(gè)Maker會(huì)將前向Op的輸入和輸出都作為反向Op的輸入,將前向Op的輸入的梯度作為反向Op的輸出,并將前向Op的屬性拷貝過(guò)來(lái)。注意:DefaultGradOpDescMaker會(huì)將前向Op的所有輸入輸出都做反向Op的輸入,即使這個(gè)輸入是沒(méi)有必要的,這將會(huì)導(dǎo)致無(wú)法對(duì)沒(méi)有用到的變量做內(nèi)存優(yōu)化。
- 框架沒(méi)有提供默認(rèn)的op_infer_var_shape方法。如果該Op是無(wú)OpKernel的,通常需要用戶添加對(duì)應(yīng)的op_infer_var_shape方法;如果該Op是有OpKernel的,需要實(shí)現(xiàn)OperatorWithKernel中的InferShape方法,此時(shí)不需要提供op_infer_var_shape方法。具體實(shí)現(xiàn)可參考while_op.cc,conv_op.cc。
- 框架沒(méi)有提供默認(rèn)的op_infer_var_type方法,用戶需要根據(jù)實(shí)際情況添加op_infer_var_type。嚴(yán)格來(lái)說(shuō)每個(gè)Op都應(yīng)該注冊(cè)一個(gè)InferVarType,op_infer_var_type根據(jù)輸入的Var的type和dtype推斷輸出Var的type和dtype。注意:在Python端的LayerHelper中create_variable_for_type_inference操作返回的Variable里面是LoDTensor,C++端的InferVarType可以修改Variable的type和dtype。
更多內(nèi)容請(qǐng)參考: 如何寫(xiě)新的Op
寫(xiě)Op注意事項(xiàng)
1.Op可以支持輸入輸出類型
Paddle的Op的輸入輸出都是Variable,從設(shè)計(jì)上講,Variable中可以存放任意類型,Op的輸入輸出Variable可能是是任意類型,通常情況下Variable中存放的是LoDTensor、SelectedRows。
注意:
? 代碼中經(jīng)常出現(xiàn)context.Input(“Input”),并不表示”Input”的Variable是Tensor,而是從”Input”的Variable的LoDTensor中獲取Tensor。如果”Input”的Variable是SelectedRows,則會(huì)報(bào)錯(cuò)。
? 如果”Input”是SelectedRows,context->GetInputDim(“Input”)返回的是var->Get().GetCompleteDims(),而不是SelectedRows中Tensor的Dim。
2.在Op內(nèi)部不能對(duì)輸入的數(shù)據(jù)做任何的改寫(xiě)
在Op內(nèi)部絕不允許對(duì)輸入數(shù)據(jù)做任何改寫(xiě),因?yàn)榭赡艽嬖谄渌鸒p需要讀這個(gè)數(shù)據(jù)。
3.OpKernel需要注冊(cè)的數(shù)據(jù)類型
目前要求所有OpKernel都要注冊(cè)double和float數(shù)據(jù)類型。
4.GetExpectedKernelType方法重寫(xiě)
GetExpectedKernelType方法是OperatorWithKernel類中用于獲取指定設(shè)備(例如CPU,GPU)上指定數(shù)據(jù)類型(例如double,float)的OpKernel的方法。該方法通過(guò)獲取輸入變量?jī)?nèi)部的Tensor數(shù)據(jù)類型得知需要的Kernel數(shù)據(jù)類型,但是由于Tensor在此處可能尚未被初始化,所以在該方法內(nèi)使用輸入變量時(shí)需要進(jìn)行必要的初始化檢查。在新增含Kernel的Op的時(shí)候,關(guān)于該方法的重寫(xiě)需要注意以下兩點(diǎn)。
4.1 僅在必要時(shí)重寫(xiě)此方法
基類OperatorWithKernel中的GetExpectedKernelType方法對(duì)于派生類Op的所有輸入變量進(jìn)行了完備的初始化檢查,建議在新增的Op中直接使用基類的此方法,例如:
? MeanOp:該Op的所有輸入變量在Run之前應(yīng)該全部被初始化,初始化檢查是必要且合理的
但是在一些情況下,直接使用基類的GetExpectedKernelType方法無(wú)法滿足需求,則需要對(duì)該方法進(jìn)行重寫(xiě),具體情況及示例如下: - OP的輸入有多個(gè),且數(shù)據(jù)類型不同,例如 AccuracyOp,需要重寫(xiě)GetExpectedKernelType方法,指定用某一輸入變量獲取kernel類型
- Op包含Dispensable的輸入變量,該類輸入變量是可選的,當(dāng)用戶未輸入時(shí),該類變量未被初始化屬于合理情況,例如 ConvOp,存在Bias等可選的輸入變量,需要重寫(xiě)GetExpectedKernelType方法,指定用必須提供的輸入變量獲取kernel類型
- Op的部分輸入變量即使未被初始化也屬于合理情況,例如 ConcatOp,輸入變量X中有個(gè)Tensor需要連接,其中可能包含未被初始化的Tensor,需要重寫(xiě)GetExpectedKernelType方法,使用輸入變量X獲取kernel的過(guò)程中,合理忽略掉部分Tensor為空的情況
- OP的Kernel類型與輸入變量無(wú)關(guān)(可能由其他參數(shù)指定),例如 FillOp,該Op沒(méi)有輸入,Kernel類型通過(guò)Op的dtype參數(shù)指定,因此需要重寫(xiě)GetExpectedKernelType方法,用參數(shù)指定的數(shù)據(jù)類型獲取kernel類型
- Op Kernel的部分參數(shù)在使用某些庫(kù)時(shí),需要指定為相應(yīng)的值,因此需要重寫(xiě)GetExpectedKernelType方法,覆蓋默認(rèn)參數(shù)
o 使用CUDNN庫(kù):需要指定OpKernel的LibraryType為kCUDNN,例如 AffineGridOp
o 使用MKLDNN庫(kù):需要指定OpKernel的LibraryType和DataLayout為kMKLDNN MulOp
4.2 重寫(xiě)此方法時(shí)需要對(duì)輸入變量進(jìn)行初始化檢查
在需要重寫(xiě)GetExpectedKernelType方法時(shí),一般會(huì)根據(jù)某一輸入變量獲取Kernel的數(shù)據(jù)類型,此時(shí)請(qǐng)使用OperatorWithKernel::IndicateVarDataType接口獲取變量的dtype,該方法對(duì)指定的輸入變量進(jìn)行了必要的初始化檢查,詳見(jiàn)Paddle PR #20044,實(shí)現(xiàn)示例如下,:
framework::OpKernelType GetExpectedKernelType(
const framework::ExecutionContext& ctx) const override {
return framework::OpKernelType(
OperatorWithKernel::IndicateVarDataType(ctx, “X”), ctx.GetPlace());
}
如果未使用帶有初始化檢查的方法,直接使用了Tensor->type(),可能會(huì)導(dǎo)致報(bào)出holder_ should not be null. Tensor not initialized yet when Tensor::type()的錯(cuò)誤,例如Paddle issue #19522 ,用戶僅憑該錯(cuò)誤信息將無(wú)法得知具體出錯(cuò)的Op,不利于調(diào)試。
5.Op兼容性問(wèn)題
對(duì)Op的修改需要考慮兼容性問(wèn)題,要保證Op修改之后,之前的模型都能夠正常加載及運(yùn)行,即新版本的Paddle預(yù)測(cè)庫(kù)能成功加載運(yùn)行舊版本訓(xùn)練的模型。所以,需要保證Op的Input、Output和Attribute不能被修改(文檔除外)或刪除,可以新增Input、Output和Attribute,但是新增的Input,Output必須設(shè)置AsDispensable,新增的Attribute必須設(shè)置默認(rèn)值。更多詳細(xì)內(nèi)容請(qǐng)參考OP修改規(guī)范:Input/Output/Attribute只能做兼容修改 。
6.ShareDataWith的調(diào)用
ShareDataWith的功能是使兩個(gè)Tensor共享底層buffer,在調(diào)用這個(gè)操作的時(shí)候需要特別注意,在Op內(nèi)部不能將ShareDataWith作用在Op的輸出上,即Op輸出的Tensor必須是Malloc出來(lái)的。
7.稀疏梯度參數(shù)更新方法
目前稀疏梯度在做更新的時(shí)候會(huì)先對(duì)梯度做merge,即對(duì)相同參數(shù)的梯度做累加,然后做參數(shù)以及附加參數(shù)(如velocity)的更新。
8.顯存優(yōu)化
8.1 為可原位計(jì)算的Op注冊(cè)Inplace
有些Op的計(jì)算邏輯中,輸出可以復(fù)用輸入的顯存空間,也可稱為原位計(jì)算。例如reshape_op中,輸出Out可以復(fù)用輸入X的顯存空間,因?yàn)樵揙p的計(jì)算邏輯不會(huì)改變X的實(shí)際數(shù)據(jù),只是修改它的shape,輸出和輸入復(fù)用同一塊顯存空間不影響結(jié)果。對(duì)于這類OP,可以注冊(cè)Inlace,從而讓框架在運(yùn)行時(shí)自動(dòng)地進(jìn)行顯存優(yōu)化。
Paddle提供了DECLARE_INPLACE_OP_INFERER宏用于注冊(cè)Inplace,該宏第一個(gè)參數(shù)是一個(gè)類名,如ReshapeOpInplaceInToOut;第二個(gè)參數(shù)是一對(duì)復(fù)用的輸入輸出,以{“X”, “Out”}的形式給出。在REGISTER_OPERATOR時(shí), 可以將類名傳傳入,從而為該Op注冊(cè)Inplace。
DECLARE_INPLACE_OP_INFERER(ReshapeOpInplaceInToOut, {“X”, “Out”});
REGISTER_OPERATOR(
reshape, ops::ReshapeOp, ops::ReshapeOpMaker,
paddle::framework::DefaultGradOpMaker<paddle::framework::OpDesc, true>,
paddle::framework::DefaultGradOpMaker<paddle::imperative::OpBase, true>,
ops::ReshapeOpInplaceInToOut);
8.2 減少OP中的無(wú)關(guān)變量
通常反向Op會(huì)依賴于前向Op的某些輸入(Input)、輸出(Output),以供反向Op計(jì)算使用。但有些情況下,反向Op不需要前向Op的所有輸入和輸出;有些情況下,反向Op只需要前向Op的部分輸入和輸出;有些情況下,反向Op只需要使用前向Op中輸入和輸出變量的Shape和LoD信息。若Op開(kāi)發(fā)者在注冊(cè)反向Op時(shí),將不必要的前向Op輸入和輸出作為反向Op的輸入,會(huì)導(dǎo)致這部分顯存無(wú)法被框架現(xiàn)有的顯存優(yōu)化策略優(yōu)化,從而導(dǎo)致模型顯存占用過(guò)高。
所以在寫(xiě)注冊(cè)反向Op時(shí)需要注意以下幾點(diǎn):
? Paddle提供的DefaultGradOpMaker,默認(rèn)會(huì)將前向op的所有輸入(Input)、輸出(Output)以及輸出變量所對(duì)應(yīng)的梯度(Output@Grad)作為反向Op的輸入,將前向Op輸入所對(duì)應(yīng)的梯度(Input@Grad)作為反向Op的輸出。所以在使用DefaultGradOpMaker時(shí)需要考慮是否有些變量在計(jì)算中不被用到。
? 如果DefaultGradOpMaker不能夠滿足需求,需要用戶自己手動(dòng)構(gòu)建GradOpMaker,具體實(shí)現(xiàn)請(qǐng)參考相關(guān)文檔;
? 如果有些反向Op需要依賴前向Op的輸入或輸出變量的的Shape或LoD,但不依賴于變量中Tensor的Buffer,且不能根據(jù)其他變量推斷出該Shape和LoD,則可以通過(guò)DECLARE_NO_NEED_BUFFER_VARS_INFERER接口對(duì)該變量(以下稱該變量為X)在反向Op中進(jìn)行注冊(cè)NoNeedBufferVars。一旦注冊(cè)了NoNeedBufferVars,反向op中就不能讀寫(xiě)該變量對(duì)應(yīng)的Tensor中的buffer,只能調(diào)用Tensor的dims()和lod()方法,同時(shí),反向Op中的GetExpectedKernelType()必須要重寫(xiě),并且GetExpectedKernelType()中不能訪問(wèn)X變量中Tensor的type()方法。比如在SliceOpGrad中只會(huì)用到Input中變量的Shape信息,所以需要為對(duì)Input在SliceOpGrad上進(jìn)行注冊(cè):
namespace paddle {
namespace operators {
// …
class SliceOpGrad : public framework::OperatorWithKernel {
public:
using framework::OperatorWithKernel::OperatorWithKernel;
void InferShape(framework::InferShapeContext* ctx) const override {
// …
}
framework::OpKernelType GetExpectedKernelType(
const framework::ExecutionContext& ctx) const override {
// Note: don’t get data type from ctx.Inputframework::Tensor(“Input”);
auto dtype = ctx.Inputframework::Tensor(framework::GradVarName(“Out”))->type();
return framework::OpKernelType( dtype, ctx.GetPlace());
}
};
template
class SliceOpGradMaker : public framework::SingleGradOpMaker {
public:
using framework::SingleGradOpMaker::SingleGradOpMaker;
protected:
void Apply(GradOpPtr bind) const override {
bind->SetInput(“Input”, this->Input(“Input”));
if (this->HasInput(“StartsTensor”)) {
bind->SetInput(“StartsTensor”, this->Input(“StartsTensor”));
}
if (this->HasInput(“EndsTensor”)) {
bind->SetInput(“EndsTensor”, this->Input(“EndsTensor”));
}
if (this->HasInput(“StartsTensorList”)) {
bind->SetInput(“StartsTensorList”, this->Input(“StartsTensorList”));
}
if (this->HasInput(“EndsTensorList”)) {
bind->SetInput(“EndsTensorList”, this->Input(“EndsTensorList”));
}
bind->SetInput(framework::GradVarName(“Out”), this->OutputGrad(“Out”));
bind->SetOutput(framework::GradVarName(“Input”), this->InputGrad(“Input”));
bind->SetAttrMap(this->Attrs());
bind->SetType(“slice_grad”);
}
};
DECLARE_NO_NEED_BUFFER_VARS_INFERER(SliceOpGradNoNeedBufferVarsInference,
“Input”);
} // namespace operators
} // namespace paddle
namespace ops = paddle::operators;
REGISTER_OPERATOR(slice, ops::SliceOp, ops::SliceOpMaker,
ops::SliceOpGradMakerpaddle::framework::OpDesc,
ops::SliceOpGradMakerpaddle::imperative::OpBase);
REGISTER_OPERATOR(slice_grad, ops::SliceOpGrad,
ops::SliceDoubleOpGradMakerpaddle::framework::OpDesc,
ops::SliceDoubleOpGradMakerpaddle::imperative::OpBase,
ops::SliceOpGradNoNeedBufferVarsInference);
9.混合設(shè)備調(diào)用
由于GPU是異步執(zhí)行的,當(dāng)CPU調(diào)用返回之后,GPU端可能還沒(méi)有真正的執(zhí)行,所以如果在Op中創(chuàng)建了GPU運(yùn)行時(shí)需要用到的臨時(shí)變量,當(dāng)GPU開(kāi)始運(yùn)行的時(shí)候,該臨時(shí)變量可能在CPU端已經(jīng)被釋放,這樣可能會(huì)導(dǎo)致GPU計(jì)算出錯(cuò)。
關(guān)于GPU中的一些同步和異步操作:
The following device operations are asynchronous with respect to the host:
Kernel launches;
Memory copies within a single device’s memory;
Memory copies from host to device of a memory block of 64 KB or less;
Memory copies performed by functions that are suffixed with Async;
Memory set function calls.
關(guān)于cudaMemCpy和cudaMemCpyAsync注意事項(xiàng):
? 如果數(shù)據(jù)傳輸是從GPU端到非頁(yè)鎖定的CPU端,數(shù)據(jù)傳輸將是同步,即使調(diào)用的是異步拷貝操作。
? 如果數(shù)據(jù)傳輸是從CPU端到CPU端,數(shù)據(jù)傳輸將是同步的,即使調(diào)用的是異步拷貝操作。
更多內(nèi)容可參考:Asynchronous Concurrent Execution,API synchronization behavior
10. LoD 在 Op 內(nèi)部的傳導(dǎo)規(guī)范
LoD 是 Paddle 框架用來(lái)表示變長(zhǎng)序列數(shù)據(jù)的屬性,除了僅支持輸入是 padding data 的 Op 外,所有 Op 的實(shí)現(xiàn)都要考慮 LoD 的傳導(dǎo)問(wèn)題。
根據(jù) OP 的計(jì)算過(guò)程中是否用到 LoD,可以將涉及到 LoD 傳導(dǎo)問(wèn)題的 OP 分為兩類: LoD-Transparent 與 LoD-Based。
類型 特點(diǎn) 示例
LoD-Transparent 計(jì)算過(guò)程不依賴 LoD,輸入是否有 LoD 不會(huì)影響計(jì)算的結(jié)果,通常是 position-wise 的計(jì)算 conv2d_op、batch_norm_op、dropout_op 等
LoD-Based 計(jì)算以序列為單位, 計(jì)算過(guò)程依賴 LoD lstm_op、gru_op、sequence_ops 等
這兩類 OP 的 LoD 傳導(dǎo)需要考慮前向和反向兩個(gè)過(guò)程。
前向傳導(dǎo)
在前向傳導(dǎo)過(guò)程,與輸入的 LoD 相比較,Op 輸出的 LoD 可能出現(xiàn)不變、改變和消失這三種情況:
? 不變:適用于所有的 LoD-Transparent OP 與部分的 LoD-Based OP。可以在InferShape 中調(diào)用 ShareLoD() 直接將輸入 Var 的 LoD 共享給輸出 Var, 可參考 lstm_op; 如果有多個(gè)輸入且都可能存在 LoD 的情況,通常默認(rèn)共享第一個(gè)輸入, 例如 elementwise_ops forward;
? 改變:適用于部分 LoD-Based OP。在實(shí)現(xiàn) OpKernel 時(shí)需考慮輸出 LoD 的正確計(jì)算,真實(shí)的 LoD 在前向計(jì)算結(jié)束后才能確定,此時(shí)仍需要在InferShape 中調(diào)用 ShareLoD(),以確保CompileTime 時(shí)對(duì) LoD Level 做了正確的傳導(dǎo),可參考 sequence_expand_op;
? 消失:適用于輸出不再是序列數(shù)據(jù)的 LoD-Based OP。此時(shí)不用再考慮前向的 LoD 傳導(dǎo)問(wèn)題,可參考 sequence_pool_op;
其它重要的注意事項(xiàng):
? 實(shí)現(xiàn) LoD-Based OP 時(shí),需要處理好 LoD 傳導(dǎo)的邊界情況,例如對(duì)長(zhǎng)度為零的輸入的支持,并完善相應(yīng)的單測(cè),單測(cè) case 覆蓋空序列出現(xiàn)在 batch 開(kāi)頭、中間和末尾等位置的情況,可參考 test_lstm_op.py
? 對(duì) LoD Level 有明確要求的 OP,推薦的做法是在 InferShape 中即完成 LoD Level的檢查,例如 sequence_pad_op。
反向傳導(dǎo)
通常來(lái)講,OP 的某個(gè)輸入 Var 所對(duì)應(yīng)的梯度 GradVar 的 LoD 應(yīng)該與 Var 自身相同,所以應(yīng)直接將 Var 的 LoD 共享給 GradVar,可以參考 elementwise ops 的 backward
Op性能優(yōu)化
1.第三方庫(kù)的選擇
在寫(xiě)Op過(guò)程中優(yōu)先使用高性能(如cudnn、mkldnn、mklml、eigen等)中提供的操作,但是一定要做benchmark,有些庫(kù)中的操作在深度學(xué)習(xí)任務(wù)中可能會(huì)比較慢。因?yàn)楦咝阅軒?kù)(如eigen等)中提供的操作為了更為通用,在性能方面可能并不是很好,通常深度學(xué)習(xí)模型中數(shù)據(jù)量較小,所以有些情況下可能高性能庫(kù)中提供的某些操作速度較慢。比如Elementwise系列的所有Op(前向和反向),Elementwise操作在模型中調(diào)用的次數(shù)比較多,尤其是Elementwise_add,在很多操作之后都需要添加偏置項(xiàng)。在之前的實(shí)現(xiàn)中Elementwise_op直接調(diào)用Eigen庫(kù),由于Elementwise操作在很多情況下需要對(duì)數(shù)據(jù)做Broadcast,而實(shí)驗(yàn)發(fā)現(xiàn)Eigen庫(kù)做Broadcast的速度比較慢,慢的原因在這個(gè)PR#6229中有描述。
2.Op性能優(yōu)化
Op的計(jì)算速度與輸入的數(shù)據(jù)量有關(guān),對(duì)于某些Op可以根據(jù)輸入數(shù)據(jù)的Shape和Op的屬性參數(shù)來(lái)選擇不同的計(jì)算方式。比如concat_op,當(dāng)axis>=1時(shí),在對(duì)多個(gè)tensor做拼接過(guò)程中需要對(duì)每個(gè)tensor做很多次拷貝,如果是在GPU上,需要調(diào)用cudaMemCopy。相對(duì)CPU而言,GPU屬于外部設(shè)備,所以每次調(diào)用GPU的操作都會(huì)有一定的額外開(kāi)銷,并且當(dāng)需要拷貝的次數(shù)較多時(shí),這種開(kāi)銷就更為凸現(xiàn)。目前concat_op的實(shí)現(xiàn)會(huì)根據(jù)輸入數(shù)據(jù)的Shape以及axis值來(lái)選擇不同的調(diào)用方式,如果輸入的tensor較多,且axis不等于0,則將多次拷貝操作轉(zhuǎn)換成一個(gè)CUDA Kernel來(lái)完成;如果輸入tensor較少,且axis等于0,使用直接進(jìn)行拷貝。相關(guān)實(shí)驗(yàn)過(guò)程在該P(yáng)R(#8669)中有介紹。
由于CUDA Kernel的調(diào)用有一定的額外開(kāi)銷,所以如果Op中出現(xiàn)多次調(diào)用CUDA Kernel,可能會(huì)影響Op的執(zhí)行速度。比如之前的sequence_expand_op中包含很多CUDA Kernel,通常這些CUDA Kernel處理的數(shù)據(jù)量較小,所以頻繁調(diào)用這樣的Kernel會(huì)影響Op的計(jì)算速度,這種情況下最好將這些小的CUDA Kernel合并成一個(gè)。在優(yōu)化sequence_expand_op過(guò)程(相關(guān)PR#9289)中就是采用這種思路,優(yōu)化后的sequence_expand_op比之前的實(shí)現(xiàn)平均快出約1倍左右,相關(guān)實(shí)驗(yàn)細(xì)節(jié)在該P(yáng)R(#9289)中有介紹。
減少CPU與GPU之間的拷貝和同步操作的次數(shù)。比如fetch操作,在每個(gè)迭代之后都會(huì)對(duì)模型參數(shù)進(jìn)行更新并得到一個(gè)loss,并且數(shù)據(jù)從GPU端到?jīng)]有頁(yè)鎖定的CPU端的拷貝是同步的,所以頻繁的fetch多個(gè)參數(shù)會(huì)導(dǎo)致模型訓(xùn)練速度變慢。
Op數(shù)值穩(wěn)定性問(wèn)題
1.有些Op存在數(shù)值穩(wěn)定性問(wèn)題
出現(xiàn)數(shù)值穩(wěn)定性的主要原因程序在多次運(yùn)行時(shí),對(duì)浮點(diǎn)型數(shù)據(jù)施加操作的順序可能不同,進(jìn)而導(dǎo)致最終計(jì)算結(jié)果不同。而GPU是通過(guò)多線程并行計(jì)算的方式來(lái)加速計(jì)算的,所以很容易出現(xiàn)對(duì)浮點(diǎn)數(shù)施加操作的順序不固定現(xiàn)象。
目前發(fā)現(xiàn)cudnn中的卷積操作、cudnn中的MaxPooling、CUDA中CudaAtomicXX、ParallelExecutor的Reduce模式下參數(shù)梯度的聚合等操作運(yùn)行結(jié)果是非確定的。
為此Paddle中添加了一些FLAGS,比如使用FLAGS_cudnn_deterministic來(lái)強(qiáng)制cudnn使用確定性算法、FLAGS_cpu_deterministic強(qiáng)制CPU端的計(jì)算使用確定性方法。
2.WITH_FAST_MATH的開(kāi)與關(guān)
如果WITH_FAST_MATH是ON,NVCC在編譯Paddle和Egien的時(shí)候會(huì)使用–use_fast_math,這樣可能會(huì)使CUDA中的一些操作在損失一定精度的情況下變快,比如log、exp、tanh等,但也會(huì)使一些操作的計(jì)算結(jié)果是錯(cuò)的,比如pow操作,具體原因請(qǐng)查看torch/DEPRECEATED-torch7-distro#132。
其它
1.報(bào)錯(cuò)信息
Enforce提示信息不能為空,并且需要寫(xiě)明,因?yàn)閳?bào)錯(cuò)信息可以更快更方便地分析出錯(cuò)誤的原因。
2.Op的數(shù)學(xué)公式
如果Op有數(shù)學(xué)公式,一定要在代碼中將數(shù)學(xué)公式寫(xiě)明,并在Python API的Doc中顯示,因?yàn)橛脩粼趯?duì)比不同框架的計(jì)算結(jié)果時(shí)可能需要了解Paddle對(duì)Op是怎么實(shí)現(xiàn)的。
**注意:**在merge到develop分支之前一定進(jìn)行公式預(yù)覽。可參考dynamic_lstmp。
3.Op變量名的命名要規(guī)范
在定義Op時(shí),Op的輸入輸出以及屬性的命名需要符合規(guī)范,具體命名規(guī)則請(qǐng)參考:name_convention。
4.Python端Op接口中參數(shù)的順序
Python API中參數(shù)的順序一般按照重要性來(lái)排,以fc為例:
def fc(input,
size,
num_flatten_dims=1,
param_attr=None,
bias_attr=None,
act=None,
is_test=False,
name=None)
總結(jié)
以上是生活随笔為你收集整理的C++ OP相关注意事项的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 如何写新的C++ OP
- 下一篇: 如何写新的Python OP