Paddle Fluid 開發者指南
Paddle Fluid 開發者指南
==1==. 為什么需要 PaddlePaddle Fluid?
兩個基礎問題
- 如何描述機器學習模型和優化過程?
- 完備自洽,表達能力足以支持潛在出現的各種計算需求
- 如何充分利用資源高效計算?
- 支持異步設備、多卡、分布式計算
- 降低計算/計算優化的開發成本
- ……
如何描述模型和優化過程?
一組連續執行的layers | variable和operator構成的計算圖 | 不再有模型的概念 | |
---|---|---|---|
2013 | Caffe,Theano, Torch, PaddlePaddle | ||
2015 | Caffe, Theano, Torch, PaddlePaddles | ||
2016 | PyTorch, TensorFlow Eager Execution, ==PaddlePaddle Fluid== |
目標 😄
- 提高對各類機器學習任務的描述能力:能夠描述潛在出現的任意機器學習模型。
- 代碼結構邏輯清晰,各模塊充分解耦:內外部貢獻者能夠專注于自己所需的功能模塊,基于框架進行再次開發。
- 從設計上,留下技術優化的空間和潛力。
- 代碼解耦后降低多設備支持、計算優化等的開發成本。
- 在統一的設計理念下,實現自動可伸縮,自動容錯的分布式計算。
==2.== Design Overview
Fluid: 系統形態
讓我們在Fluid程序實例中,區分編譯時和運行時
Fluid 編譯時
- ==定義前向計算==
- x = fluid.layers.data(name='x',shape=[13], dtype='float32')
- y_predict = fluid.layers.fc(input=x, size=1, act=None)
- y = fluid.layers.data(name='y', shape=[1], dtype='float32')
- cost = fluid.layers.square_error_cost(input=y_predict, label=y)
avg_cost = fluid.layers.mean(x=cost)
- ==添加反向、正則、優化==
- learning_rate = 0.01
- sgd_optimizer = fluid.optimizer.SGD(learning_rate)
sgd_optimizer.minimize(avg_cost)
Program vs. 計算圖
- 在科學計算領域,計算圖是一種描述計算的經典方式。下圖展示了從前向計算圖(藍色)開始,通過添加反向(紅色)和優化算法相關(綠色)操作,構建出整個計算圖的過程:
- Fluid ==使用Program而不是計算圖==來描述模型和優化過程。Program由Block、Operator和Variable構成,相關概念會在后文詳細展開。
- 編譯時 Fluid 接受前向計算(這里可以先簡單的理解為是一段有序的計算流)Program,為這段前向計算按照:前向 ➡反向 ➡ 梯度 clip ➡ 正則 ➡ 優化 的順序,添加相關 Operator和Variable到Program到完整的計算。
Fluid 運行時
- ==讀入數據==
- train_reader = paddle.batch(
- paddle.reader.shuffle(paddle.dataset.uci_housing.train(), buf_size=500),
- batch_size=20)
feeder = fluid.DataFeeder(place=place, feed_list=[x, y])
- ==定義執行程序的設備==
- place = fluid.CPUPlace()
feeder = fluid.DataFeeder(place=place,feed_list=[x, y])
- ==創建執行器(Executor),執行初始化 Program和訓練Program==
- exe = fluid.Executor(place)
- exe.run(fluid.default_startup_program())
- PASS_NUM = 100
- for pass_id in range(PASS_NUM):
- for data in train_reader():
- avg_loss_value, = exe.run(fluid.default_main_program(),
- feed=feeder.feed(data),
- fetch_list=[avg_cost])
print(avg_loss_value)
總結:框架做什么?用戶做什么?
構建訓練 | 執行訓練 |
---|---|
用戶:描述前向運算 框架:添加反向運算 框架:添加優化運算 框架:添加內存優化 框架:添加并行/多設備/分布式相關的計算單元 |
框架:創建Operator(計算)+ Variable(數據) 框架:創建Block 框架:內存管理/設備管理 框架:執行計算 |
總結:編譯時
用戶編寫一段Python程序,描述模型的前向計算
- 創建變量描述 VarDesc
- 創建operators的描述 OpDesc
- 創建operators的屬性
- 推斷變量的類型和形狀,進行靜態檢查:inferShape
- 規劃變量的內存復用
- 創建反向計算
- 添加優化相關的Operators
- (可選)添加多卡/多機相關的Operator,生成在多卡/多機上運行的程序
總結:運行時
執行規劃好的計算
- 創建Executor
- 為將要執行的一段計算,在層級式的Scope空間中創建Scope
- 創建Block,依次執行Block
Figure. 編譯時運行時概覽
==3==. 用戶如何描述計算?
Fluid:==像寫程序一樣==定義計算
- 順序執行
- x = fluid.layers.data(name='x',shape=[13], dtype='float32')
- y_predict = fluid.layers.fc(input=x, size=1, act=None)
- y = fluid.layers.data(name='y', shape=[1], dtype='float32')
cost = fluid.layers.square_error_cost(input=y_predict, label=y)
- 條件分支: swith、ifelse
- a = fluid.Var(10)
- b = fluid.Var(0)
- switch = fluid.switch()
- with switch.block():
- with switch.case(fluid.less_equal(a, 10)):
- fluid.print("Case 1")
- with switch.case(fluid.larger(a, 0)):
- fluid.print("Case 2")
- with switch.default():
fluid.print("Case 3")
Fluid: ==像寫程序一樣==定義計算
- 循環:while
- d0 = layers.data("d0", shape=[10], dtype='float32')
- data_array = layers.array_write(x=d0, i=i)
- array_len = layers.fill_constant(shape=[1],dtype='int64', value=3)
- cond = layers.less_than(x=i, y=array_len)
- while_op = layers.While(cond=cond)
- with while_op.block():
- d = layers.array_read(array=data_array, i=i)
- i = layers.increment(x=i, in_place=True)
- layers.array_write(result, i=i, array=d)
layers.less_than(x=i, y=array_len, cond=cond)
總結
- 用戶層提供的描述語法具有完備性、自洽性,有能力支持對復雜計算過程描述
- 使用方式和核心概念可以類比編程語言,認知能夠直接遷移
- 能夠支持:定義問題,逐步求解
==3.== 核心概念
編譯時概念 :==變量和計算的描述==
- VarDesc + TensorDesc + OpDesc ➡ BlockDesc ➡ ProgramDesc
- 什么是 Fluid Program
- 在Fluid中,一個神經網絡任務(訓練/預測)被描述為一段Program
- Program包含對Variable(數據)和 Operator(對數據的操作)的描述
- Variable 和 Operator 被組織為多個可以嵌套的Block,構成一段完整的Fluid Program
編譯階段最終,經過 Transpiler 的執行規劃,變換處理,生成使用protobuf序列化后的ProgramDesc。可以發送給多卡或者網絡中的其它計算節點執行
編譯時概念 :==Transpiler==
- 接受一段ProgramDesc作為輸入,生成一段新的ProgramDesc
- Memory optimization transpiler:向原始ProgramDesc 中插入 FreeMemoryOps,在一次迭代優化結束前提前釋放內存,使得能夠維持較小的 memory footprint
- Distributed training transpiler:將原始的ProgramDesc中轉化為對應的分布式版本,生成兩段新的ProgramDesc:
- trainer進程執行的ProgramDesc
- parameter server執行的ProgramDesc
- ==WIP==: 接受一段ProgramDesc,生成可直接被gcc, nvcc, icc等編譯的代碼,編譯后得到可執行文件
Transplier
打印 ProgramDesc
- default_startup_program:創建可學習參數,對參數進行初始化
- default_main_program:由用戶定義的模型,包括了前向、反向、優化及所有必要的計算
- 打印可讀的 Program
- from paddle.v2.fluid import debuger
print debuger.pprint_program_codes(framework.default_main_program().desc)
輸出效果
variable in block 0 | variable in block 0 |
---|---|
![]() |
![]() |
運行時概念
- 數據相關
- Tensor / LoDTensor / Variable
- Scope
- 計算相關
- Block
- Kernel、OpWithKernel、OpWithoutKernel
protobuf messages | C++ class objects | |
---|---|---|
Data | VarDesc | Variable |
Operation | OpDesc | Operator |
Block | BlockDesc | Block |
- 執行相關 :Executor
Tensor 和 LoD(Level-of-Detail) Tensor
- Tensor 是$n$-dimensional arry的推廣,LoDTensor是在Tensor基礎上附加了序列信息
- Fluid中輸入、輸出,網絡中的可學習參數全部統一使用LoDTensor(n-dimension array)表示
- 一個mini-batch輸入數據是一個LoDTensor
- 在Fluid中,RNN 處理變長序列無需padding,得益于 LoDTensor表示
- 可以簡單將 LoD 理解為:std::vector<std::vector<int>>
- 對非序列數據,LoD 信息為空
TensorFlow | PaddlePaddle | |
---|---|---|
RNN | Support | Support |
recursive RNN | Support | Support |
padding zeros | Must | No need |
blob data type | Tensor | LoDTensor |
LoD 信息實例
- 圖(a)的LoD 信息
[0, 5, 8, 10, 14]
- 圖(b)的 LoD 信息
[[0, 5, 8, 10, 14] /*level=1*/, [0, 2, 3, 5, 7, 8, 10, 13, 14] /*level=2*/]
Tensor, Variable, Scope 之間的關系
- Block 是一個實現層的概念,不在應用層暴露給用戶。目前用戶無法自行創建并利用Block,用戶能夠感知的只有Program這個概念。
- 邏輯上,可以將 Block 類比為編程語言中的大括號:定義了一段作用域,其中運行一段代碼
- Executor會為每一個Block創建一個Scope,Block是可嵌套的,因此Scope也是可嵌套的
Executor
接口 | 說明 |
---|---|
![]() |
輸入 1. ProgramDesc 2. Scope 3.block_id解釋執行步驟 1. 創建所有 Variables 2. 逐一創建 Operator 并運行 |
Operator/OpWithKernel/Kernel
- operator 無狀態,Operator的核心是==Run==方法
- 一個operator可以注冊多個kernel
- operator 可以無 kernel:while_op 、ifelse op
Fluid Operator vs. PaddlePaddle layers
Layer | Operator |
---|---|
![]() |
![]() |
1. 內部維護狀態 2. 包含forward和backward方法 |
1. 內部無狀態 2. 只有Run方法 |
==4.== 內存管理
目標
- 為異構設備提供統一的內存分配、回收接口
- 最小化管理內存所需的時間,最小化管理開銷
- 減少內存碎片
- 將內存管理與計算(Operators/Kernels)完全剝離
- 統一內存管理是內存優化的基礎
Memory 接口
- 內存管理模塊向上層應用邏輯提供三個基礎接口:
- template <typename Place>
- void* Alloc(Place place, size_t size);
- template <typename Place>
- void Free(Place place, void* ptr);
- template <typename Place>
- size_t Used(Place place);
- struct Usage : public boost::static_visitor<size_t> {
- size_t operator()(const platform::CPUPlace& cpu) const;
- size_t operator()(const platform::CUDAPlace& gpu) const;
};
- 模板參數 Place 指示內存分配發生的設備
- 實現時,需特化支持的 Place, 提供以上三個接口的實現
代碼結構
內存管理模塊可以理解為由以下兩部分構成:
- SystemAllocator:實際從物理設備上分配、釋放的內存的接口
- BuddyAllocator:內存管理算法
System Allocator
- SystemAllocator 是實現物理內存分配、回收的基類
- 不同設備上的內存分配和回收終將轉化為標準接口調用
- 為不同設備實現MemoryAllocator,繼承自SystemAllocator
- class SystemAllocator {
- public:
- virtual ~SystemAllocator() {}
- virtual void* Alloc(size_t& index, size_t size) = 0;
- virtual void Free(void* p, size_t size, size_t index) = 0;
- virtual bool UseGpu() const = 0;
};
CPU/GPU Allocator
class CPUAllocator : public SystemAllocator {
public:
virtual void* Alloc(size_t& index, size_t size);
virtual void Free(void* p, size_t size, size_t index);
virtual bool UseGpu() const;
};
#ifdef PADDLE_WITH_CUDA
class GPUAllocator : public SystemAllocator {
public:
virtual void* Alloc(size_t& index, size_t size);
virtual void Free(void* p, size_t size, size_t index);
virtual bool UseGpu() const;
private:
size_t gpu_alloc_size_ = 0;
size_t fallback_alloc_size_ = 0;
};
#endif
- CPUAllocator和GPUAllocator分別繼承自SystemAllocator,分別調用相應的標準庫函數實現物理內存的分配和釋放。
- 一旦大塊、連續的物理內存分配之后,將通過內存管理算法實現內存的按塊分配、回收、重用等。
CPU Allocator
- CPU 內存的分配提供兩種選項:
- non-pinned memory:可分頁內存
- pinned memory:頁鎖定內存
- 分配過大的頁鎖定內存有可能因為系統可使用的分頁內存減少,影響系統性能,默認CPU下分配的是可分頁內存
- 通過gflags進行設置一次性分配內存的大小以及是否使用頁鎖定內存。
- DEFINE_bool(use_pinned_memory, true, "If set, allocate cpu pinned memory.");
- DEFINE_double(fraction_of_cpu_memory_to_use, 1,
- "Default use 100% of CPU memory for PaddlePaddle,"
"reserve the rest for page tables, etc");
GPU Allocator
- 通過 cudaMalloc 分配GPU顯存
- GPUAllocator::Alloc 首先會計算指定GPU device上的可用顯存
- 如果可用顯存小于請求分配大小,調用cudaMalloc進行分配
- 如果可用顯存不足,目前會報錯退出。
- 通過gflags控制GPU下一次性分配顯存的大小:
- DEFINE_double(fraction_of_gpu_memory_to_use, 0.92,
- "Default use 92% of GPU memory for PaddlePaddle,"
"reserve the rest for page tables, etc");
內存管理算法: Buddy Memory Allocation
- Memory Arena:一次性分配大塊連續內存,之后會基于這塊內存進行內存管理:動態分配、釋放、重用內存塊。
- 伙伴內存分配:
- 將內存劃分為 2 的冪次方個分區,使用 best-fit 方法來分配內存請求。
- 當釋放內存時,檢查 buddy 塊,查看相鄰的內存塊是否也已被釋放。如果是,將內存塊合并,以最小化內存碎片。
- 分配的內存在物理內存的自然邊界對齊,提高內存訪問效率。
- 算法的時間效率高,單使用 best-fit 方法的緣故,會產生一定的內存浪費
Buddy Allocator
- BuddyAllocator 是一個單例,每個設備(如: GPU/CPU(0)/GPU(1)) 擁有一個BuddyAllocator
- BuddyAllocator 內部擁有一個私有成員變量 SystemAllocator
- 當請求的內存超過BuddyAllocator管理的空余內存時,將會調用SystemAllocator去指定的設備上分配物理內存
實例:CPU 下內存管理接口的實現
- 對上層應用,統一通過BuddyAllocator來實現內存的分配、釋放以及用量查詢
- template <>
- void* Alloc<platform::CPUPlace>(platform::CPUPlace place, size_t size) {
- VLOG(10) << "Allocate " << size << " bytes on " << platform::Place(place);
- void* p = GetCPUBuddyAllocator()->Alloc(size);
- VLOG(10) << " pointer=" << p;
- return p;
- }
- template <>
- void Free<platform::CPUPlace>(platform::CPUPlace place, void* p) {
- VLOG(10) << "Free pointer=" << p << " on " << platform::Place(place);
- GetCPUBuddyAllocator()->Free(p);
- }
- template <>
- size_t Used<platform::CPUPlace>(platform::CPUPlace place) {
- return GetCPUBuddyAllocator()->Used();
}
==5.== 多設備支持
多設備支持(一)
- step 1:添加Place類型,由用戶實現添加到框架
- 可以將Place類型理解為一個整數加上一個枚舉型,包括:設備號 + 設備類型
- DeviceContext
- 不同的Place會對應一個相應的DeviceContext,用于組織管理與設備相關的信息
- 例如,GpuDeviceContext中會管理Cuda stream
- 目前實現中一些特殊的庫也會對應有自己的DeviceContext:例如:
- 不同的Place會對應一個相應的DeviceContext,用于組織管理與設備相關的信息
class MKLDNNDeviceContext : public CPUDeviceContext {……}
-
- 每種設備對應的DeviceContext需要管理的內容不盡相同,視具體需求來實現
多設備支持(二)
- step 2: 增加KernelType,為相應的KernelType注冊Kernel對象,由用戶實現注冊給框架 可以按照:
- Place 執行設備
- DataType 執行數據類型 FP32/FP64/INT32/INT64
- Memory layout: 運行時 Tensor 在內存中的排布格式 NCHW、 NHWC
- 使用的庫
來區分Kernel,為同一個operator注冊多個 Kernel。
struct OpKernelType {
proto::DataType data_type_;
DataLayout data_layout_;
platform::Place place_;
LibraryType library_type_;
}
多設備支持(三)
step 3: 運行時的 KernelType 推斷和Kernel切換,按需要修改Kernel推斷和Kernel切換規則
- Expected Kernel:期待調用的Kernel:由(1)Place和計算精度決定;或(2)用戶在配置中顯示指定使用的計算庫,如cudnn、mkldnn等。
- Actual Kernel:運行時從Operator的輸入(Variable)可以推斷出實際需要的KernelType
- 當Expected Kernel和Actual Kernel不一致的時候,框架會插入data_transformer或者data_layerout_transform等,保證Expected Kernel可以執行,包括:
- CPUPlace ➡ GPUPlace :跨設備內存復制
- NCHW ➡ nChw8c :Layout轉換
- FP32 ➡ FP16 :精度轉換 尚未支持
- ……
- 以上過程實現在OperatorWithKernel類的Run方法中 ➡
==6.== while_op
while_op
- 循環執行一段Program,直到條件operator判斷循環條件不滿足時終止循環
- while_op 的特殊之處:
- while_op 沒有 kernel
- while_op 擁有自己的Block,會形成一段嵌套的Block
- ==while_op 內部創建了一個 Executor,來循環執行Block==
- while_op 輸入輸出 : LoDTensorArray
- namespace paddle {
- namespace framework {
- using LoDTensorArray = std::vector<LoDTensor>;
- }
}
-
- 每一次循環,從原始輸入中“切出”一個片段
- LoDTensorArray 在Python端暴露,是Fluid支持的基礎數據結構之一,用戶可以直接創建并使用
while_op Run 方法概覽
void Run(const framework::Scope &scope,
const platform::Place &dev_place) const override {
PADDLE_ENFORCE_NOT_NULL(scope.FindVar(Input(kCondition)));
auto &cond = scope.FindVar(Input(kCondition))->Get<LoDTensor>();
PADDLE_ENFORCE_EQ(cond.dims(), paddle::framework::make_ddim({1}));
framework::Executor executor(dev_place);
auto *block = Attr<framework::BlockDesc *>(kStepBlock);
auto *program = block->Program();
auto step_scopes =
scope.FindVar(Output(kStepScopes))->GetMutable<StepScopeVar>();
while (cond.data<bool>()[0]) {
auto ¤t_scope = scope.NewScope();
step_scopes->push_back(¤t_scope);
executor.Run(*program, ¤t_scope, block->ID(),
false /*create_local_scope*/);
}
}
while_op 的重要應用:Dynamic RNN
什么是 dynamicRNN ?
- 用戶可以自定義在一個時間步之內的計算, 框架接受序列輸入數據,在其上循環調用用戶定義的單步計算
- 可學習參數在多個時間步之間共享
- dynamicRNN 由 while_op 實現
- 如果dynamicRNN中定義了memory,將會構成一個循環神經網絡,否則其行為就等于在輸入序列上循環調用預定義的單步計算
dynamic RNN 用戶接口
- dynamicRNN 中的重要元素
- step input: dynamicRNN 每個時間步的輸入
- step function: 用戶定義的單步計算
- memory: 用于形成循環連接
- external/static memory:單步計算的每一步都可以全部讀取到的外部輸入
dynamicRNN 中的 Memory
dynamicRNN中memory的行為非常類似于 C++ 中的引用變量
- memory “指向” 一個operator的輸出變量,記作: A
- memory 可以被 LoDTensor 初始化(當LoD信息為空時,為非序列,否則為序列),默認memory被初始化為零
- memory 在 operator A 前向計算之后,進行前向計算
- 當 memory 的前向計算會 "指向" A 的輸出 LoDTensor
- memory 的輸出可以是另一個 operator 的輸入,于是形成了“循環”連接
DynamicRNN 實現細節
- while_op 無法獨立構成dynamicRNN,必須和一組相關的 operator 及數據結構配合
- 依賴的 operators (這里僅列出最重要的,并非全部):
- lod_rank_table operator
- lod_tensor_to_array operator
- array_to_lod_tensor operator
- shrink_memory operator
- 依賴的數據結構
- TensorArray
- LoDRankTable
- 依賴的 operators (這里僅列出最重要的,并非全部):
- 在Fluid中,RNN接受變長序列輸入,無需填充,以上數據結構和相關的operator配合工作,實現了對變長輸入以batch計算
dynamicRNN 如何實現 batch 計算 ?
- 問題:
- RNN 可以看作是一個展開的前向網絡,前向網絡的深度是最長序列的長度
- 如果不對變長序列進行填充,將它們填充到一樣長度,每個mini-batch輸入將會不等長,每個樣本展開長度不一致,導致前向和反向計算實現困難
實例 :RNN encoder-decoder with attention
- 以機器翻譯的RNN encoder-decoder 模型(涉及了dynamicRNN的所有設計要素)為例,下圖是 RNN encoder-decoder 的原始輸入:
Figure. RNN encoder-decoder 原始batch 輸入數據
- source word sequences 是encoder RNN的輸出,是一個LoDTensor
- target word sequences 是look_uptable的輸入,是一個LoDTensor
- 上圖中一個矩形方塊是CPU/GPU內存中一片連續的內存空間,表示一個dense vector
dynamicRNN 如何實現 batch 計算 ?
- 對一個mini batch中不等長樣本進行排序,最長樣本變成batch中的第一個,最短樣本是batch中最后一個
- LoDTensor ➡ LoDRankTable ➕ lod_rank_table operaator
- 可以將LoDRankTable理解為對LoDTensor中的多個序列按照長度排序LoDRankTable 存儲了排序之后的index
- LoDTensor ➡ LoDRankTable ➕ lod_rank_table operaator
- 構建每個時間步的batch輸入:隨著時間步增加,每個時間步的batch輸入可能會逐漸縮小
- TensorArray ➕ lod_tensor_to_array ➡ LoDTensor (without LoD)
- 每個時間步輸出寫入一個輸出 LoDTensorArray
- dynamicRNN循環結束后, 按照LoDRankTable中記錄的信息對輸出LoDTensorArray重排序,還原會原始輸入順序
- TensorArray ➕ array_to_lod_tensor ➡ LoDTensor
運行實例
運行實例
- 執行到第5~7個batch時,batch size將會縮小
運行實例
- 第5 ~ 7個batch時RNN的memory會發生什么?
- memory 指向某個operator的輸出Tensor,在該operator前向計算之后,“取回”其計算結果
- 5 ~ 7時,遇到了序列的結束,==下一個時間步計算不再需要在已經結束的序列上展開==
- 在dynamicRNN中shrink_memory operator 用來縮小memory的batch輸入
運行實例:batch 1 ~ 2
Figure. 第1、2個batch輸入dynamicRNN的batch輸入
運行實例:batch 3 ~ 4
Figure. 第3、4個batch輸入dynamicRNN的batch輸入
運行實例:batch 5 ~ 7
Figure. 第5、6、7個batch輸入dynamicRNN的batch輸入
==7.== Fluid 代碼結構
Fluid 代碼結構
代碼結構 | 模塊結構 |
---|---|
![]() |
![]() |
==8.== 文檔總結
- 設計概覽
- 核心概念
- 重要功能模塊
- 開發指南
==9.== 開發指南
建議開發環境:使用 Docker 編譯和測試
Docker編譯PaddlePaddle源碼: ➡
PaddlePaddle 在 Dockerhub 地址:➡
- 獲取PaddlePaddle的Docker鏡像
docker pull paddlepaddle/paddle:latest-dev
- 啟動 docker container
docker run -it -v $PWD/Paddle:/paddle paddlepaddle/paddle:latest-dev /bin/bash
- 進入docker container后,從源碼編譯,請參考文檔 ➡
一些說明
- PaddlePaddle的Docker鏡像為了減小體積,默認沒有安裝vim,可以在容器中執行apt-get install -y vim來安裝vim。
- 開發推薦使用tag為latest-dev的鏡像,其中打包了所有編譯依賴。latest及lastest-gpu是production鏡像,主要用于運行PaddlePaddle程序。
- 在Docker中運行GPU程序,推薦使用nvidia-docker,否則需要將CUDA庫和設備掛載到Docker容器內。
nvidia-docker run -it -v $PWD/Paddle:/paddle paddlepaddle/paddle:latest-dev /bin/bash
- ==提交PullRequest前請務必閱讀==: ➡
- 代碼要求
- 代碼注釋遵守 Doxygen 的樣式
- 確保編譯器選項 WITH_STYLE_CHECK 已打開,并且編譯能通過代碼樣式檢查
- 所有代碼必須具有單元測試,且能夠通過所有單元測試
- 使用 pre-commit 鉤子提交Pull Request
- 幫助格式化源代碼(C++,Python)
- 在提交前自動檢查一些基本事宜:如每個文件只有一個 EOL,Git 中不要添加大文件等
- 安裝pre-commit,并在PaddlePaddle根目錄運行:
- ➜ pip install pre-commit
➜ pre-commit install
==10.== 添加新的 Operator
概念簡介
添加一個新的operator,會涉及實現以下C++類的派生類:
- framework::OperatorBase: Operator(簡寫,Op)基類。
- framework::OpKernel: Op計算函數的基類,稱作Kernel。
- framework::OperatorWithKernel:繼承自OperatorBase,Op有計算函數,稱作有Kernel。
- class OpProtoAndCheckerMaker:描述該Op的輸入、輸出、屬性、注釋,主要用于Python API接口生成
依據是否包含kernel,可以將Op分為兩種:
- 包含Kernel的Op:繼承自OperatorWithKernel,==絕大多數operator都屬于這一類==
- 不包含kernel的Op,繼承自OperatorBase,只有少量Op屬于這一類,例如while_op,ifelse_op
這里主要介紹帶Kernel的Op如何編寫。
添加新的Operator需要修改/添加哪些文件?
內容 | 定義位置 |
---|---|
OpProtoMake定義 | .cc文件,Backward Op不需要OpProtoMaker |
Op定義 | .cc文件 |
Kernel實現 | CPU、CUDA共享Kernel實現在.h文件中,否則,CPU 實現在.cc文件中,CUDA 實現在.cu文件中。 |
注冊Op | Op注冊實現在.cc文件;Kernel注冊CPU實現在.cc文件中,CUDA實現在.cu文件中 |
- 添加 Operator 之前請閱讀:Operator 命名規范及Operator Markdown注釋規范。
- 實現新的op都添加至目錄paddle/operators下,文件命名以*_op.h(如有) 、 *_op.cc 、*_op.cu(如有)結尾。
- 根據文件名自動構建op和Python端綁定,請務必遵守以上命名,否則需要進一步修改PyBind相關文件及CMakeLists.txt。
實現帶Kernel的Operator step1: 定義ProtoMaker類
下面均以clip_op為例進行介紹
- clip_op計算公式:$Out = \min(\max(X, min), max)$
- 首先定義ProtoMaker來描述該Op的輸入、輸出,并添加注釋(下面代碼段的中注釋進行了簡化,實現時需按照規范添加注釋):
- template <typename AttrType>
- class ClipOpMaker : public framework::OpProtoAndCheckerMaker {
- public:
- ClipOpMaker(OpProto* proto, OpAttrChecker* op_checker)
- : OpProtoAndCheckerMaker(proto, op_checker) {
- AddInput("X","(Tensor)The input of clip op.");
- AddOutput("Out", "(Tensor),The output of clip op.");
- AddAttr<AttrType>(
- "min", "(float),Minimum value.");
- AddAttr<AttrType>(
- "max", "(float),Maximum value.");
- AddComment(R"DOC(
- ……
- )DOC");
- }
};
實現帶Kernel的Operator step2: 定義Operator類
下面的代碼段實現了clip_op的定義:
class ClipOp : public framework::OperatorWithKernel {
public:
using framework::OperatorWithKernel::OperatorWithKernel;
void InferShape(framework::InferShapeContext* ctx) const override {
PADDLE_ENFORCE(ctx->HasInput("X"),
"Input(X) of ClipOp should not be null.");
PADDLE_ENFORCE(ctx->HasOutput("Out"),
"Output(Out) of ClipOp should not be null.");
auto x_dims = ctx->GetInputDim("X");
auto max = ctx->Attrs().Get<float>("max");
auto min = ctx->Attrs().Get<float>("min");
PADDLE_ENFORCE_LT(min, max, "max should be greater than min.");
ctx->SetOutputDim("Out", x_dims);
ctx->ShareLoD("X", /*->*/ "Out");
}
};
Operator 類中需要完成的工作
- clip_op 繼承自OperatorWithKernel,
using framework::OperatorWithKernel::OperatorWithKernel;
表示使用基類OperatorWithKernel的構造函數。
- 重寫InferShape接口。
- InferShape 為const函數,不能修改Op的成員變
- InferShape 的參數為 const framework::InferShapeContext &ctx,從中可獲取到輸入輸出以及屬性
- InferShape 會被調用兩次,一次是編譯時(創建op),一次是運行時(調用op的Run方法時),需要完成以下功能:
- 做檢查, 盡早報錯:檢查輸入數據維度、類型等是否合法
- 設置輸出Tensor的形狀
通常OpProtoMaker和Op類的定義寫在.cc文件中。
補充說明
- InferShape目前支持兩種實現方式,二者最后都會生成一個functor注冊給OpInfo結構體。
- 什么是functor ?
- 類或結構體僅重載了(),一般是可被多個kernel復用的計算函數。
- template <typename T>
- class CrossEntropyFunctor<platform::CPUDeviceContext, T> {
- public:
- void operator()(const platform::CPUDeviceContext& ctx,
- framework::Tensor* out,
- const framework::Tensor* prob,
- const framework::Tensor* labels, const bool softLabel) {
- ……
- }
};
-
- 在 clip_op 內也會看到將一段計算函數抽象為functor的使用法: ➡。
實現帶Kernel的Operator step3: 定義OpKernel類
- ClipKernel繼承自framework::OpKernel,帶有下面兩個模板參數:
- typename DeviceContext: 表示設備類型,不同設備共享同一個Kernel時,需添加該模板參數。不共享時,需要提供針對不同設備的特化實現。
- typename T : 表示支持的數據類型,如float, double等
- 在ClipKernel類中重寫Compute方法
- Compute接受輸入參數:const framework::ExecutionContext& context
- ExecutionContext 是從 Scope中將運行時Op的輸入、輸出Variable組織在一起,使得Op在調用Compute方法時,能夠簡單地通過名字拿到需要的輸入輸出Variable
- 與InferShapeContext相比,ExecutionContext 中增加了設備類型
- 在Compute函數里實現OpKernel的具體計算邏輯
- Compute接受輸入參數:const framework::ExecutionContext& context
ClipKernel 代碼概覽
template <typename DeviceContext, typename T>
class ClipKernel : public framework::OpKernel<T> {
public:
void Compute(const framework::ExecutionContext& context) const override {
auto max = context.Attr<T>("max");
auto min = context.Attr<T>("min");
auto* x = context.Input<Tensor>("X");
auto* out = context.Output<Tensor>("Out");
T* out_data = out->mutable_data<T>(context.GetPlace());
const T* x_data = x->data<T>();
int64_t numel = x->numel();
Transform<DeviceContext> trans;
trans(context.template device_context<DeviceContext>(), x_data,
x_data + numel, out_data, ClipFunctor<T>(min, max));
}
};
- 為了使OpKernel的計算過程書寫更加簡單,并且CPU、CUDA的代碼可以復用, Fluid 使用 Eigen 作為基礎的矩陣運算庫
- Fluid對Eigen unsupported Tensor提供了一些基本的封裝,可以在Compute接口中直接調用
- 關于在PaddlePaddle中如何使用Eigen庫,請參考使用文檔。
實現帶Kernel的Operator step4: 實現反向Op
- ==反向Op沒有ProtoMaker==,除此之外定義與實現方式前向Op完全一致,不再贅述
- 這里僅對反向Op的輸入輸出進行說明:
- 反向Op的輸入
- 前向Op的輸出
- 反向傳播過程中傳遞給當前Op的梯度
- 需要注意,Fluid中,不區分Cost Op和中間層Op,所有Op都必須正確處理接收到的梯度
- 反向Op的輸出
- 對可學習參數的求導結果
- 對所有輸入的求導結果
- 反向Op的輸入
實現帶Kernel的Operator step5: 注冊Op及Kernel
至此Op和Op kernel都已經實現完畢,接下來,需要在.cc和cu文件中注冊op和kernel
- 在.cc文件中注冊前向、反向Op類,注冊CPU Kernel。
- namespace ops = paddle::operators;
- REGISTER_OP(clip, ops::ClipOp, ops::ClipOpMaker<float>, clip_grad,
- ops::ClipOpGrad);
- REGISTER_OP_CPU_KERNEL(
- clip, ops::ClipKernel<paddle::platform::CPUDeviceContext, float>);
- REGISTER_OP_CPU_KERNEL(
clip_grad, ops::ClipGradKernel<paddle::platform::CPUDeviceContext, float>);
-
- 在上面的代碼片段中:
- REGISTER_OP : 注冊ops::ClipOp類,類型名為clip,該類的ProtoMaker為ops::ClipOpMaker,注冊ops::ClipOpGrad,類型名為clip_grad
- REGISTER_OP_WITHOUT_GRADIENT : 用于注冊沒有反向的Op,例如:優化算法相關的Op
- REGISTER_OP_CPU_KERNEL :注冊ops::ClipKernel類,并特化模板參數為paddle::platform::CPUPlace和float類型,同理,注冊ops::ClipGradKernel類
- 在上面的代碼片段中:
- 按照同樣方法,在.cu文件中注冊GPU Kernel
- 如果CUDA Kernel的實現基于Eigen,需在 .cu的開始加上宏定義 #define EIGEN_USE_GPU
編譯和Python端綁定
- 運行下面命令可以僅編譯新添加的Op:
- make mul_op
- 需注意,運行單元測試需要編譯整個工程
- 如果遵循前文的文件命名規則,構建過程中,會自動為新增的op添加Python端綁定,并鏈接到生成的lib庫中
實現帶Kernel的Operator step6: 添加前向單測及梯度檢測
- 新增Op的單元測試統一添加至:python/paddle/v2/fluid/tests目錄
- 前向Operator單測
- Op單元測試繼承自OpTest,各項具體的單元測試在TestClipOp里完成,所有單測case都以TestXX命名
- 單元測試Operator,需要:
- 在setUp函數定義輸入、輸出,以及相關的屬性參數
- 生成隨機的輸入數據
- 在Python腳本中實現與前向operator相同的計算邏輯,得到輸出值,與operator前向計算的輸出進行對比
- 反向梯度檢測流程測試框架已經實現,直接調用相應接口check_grad即可
- clip_op 單測代碼請參考 ➡,這里不再展開
編譯執行單測
- python/paddle/v2/framework/tests 目錄下新增的 test_*.py 單元測試會被自動加入工程進行編譯
- 運行單元測試測時需要編譯整個工程,并且編譯時需要打開WITH_TESTING, 即cmake paddle_dir -DWITH_TESTING=ON
- 編譯成功后,執行下面的命令來運行單元測試:
make test ARGS="-R test_mul_op -V"
或者:
ctest -R test_mul_op
添加Op的一些注意事項
- 為每個Op創建單獨的*_op.h(如有)、*_op.cc和*_op.cu(如有)。不允許一個文件中包含多個Op,將會導致編譯出錯。
- 注冊Op時的類型名,需要和該Op的名字一樣。不允許在A_op.cc里面,注冊REGISTER_OP(B, ...),會導致單元測試出錯。
- 如果Op沒有實現CUDA Kernel,不要創建空的*_op.cu,會導致單元測試出錯。
- 如果多個Op依賴一些共用的函數,可以創建非*_op.*格式的文件來存放,如gather.h文件。
==10.== 使用相關問題
定義前向計算
- 當在python端執行時:
import paddle.v2.fluid as fluid
framework.py定義了兩個全局Program:
# program is a global instance.
_main_program_ = Program()
_startup_program_ = Program()
- 前向定義的過程就是不斷往mian_program中添加Op和Variable
- 如果需要執行一個新的mian_program時,可以調用調用:
- def switch_main_program(program):
- """
- Switch the main program to a new program.
- This funtion returns the previous main program.
- """
……
自定義參數的初始化
- 調用fluid.ParamAttr(……)接口,自定義參數的初始化
- w_param_attrs = ParamAttr(name=None,
- initializer=UniformInitializer(low=-1.0, high=1.0, seed=0),
- learning_rate=1.0,
- regularizer=L1Decay(1.0),
- trainable=True,
- clip=GradientClipByValue(-1.0, 1.0),
- )
y_predict = fluid.layers.fc(input=x, size=1, param_attr=w_param_attrs)
- 補充問題:如何創建 Variable
- cur_program = Program()
- cur_block = cur_program.current_block()
new_var = cur_block.create_var(name="X", shape=[-1, 16, 16], dtype="float32")
添加反向Op
- 調用fluid.backward.append_backward(X)(X是一個Variable),來為一段前向ProgramDesc添加反Op
- data = fluid.layers.data(name="data", shape=(2,3,4))
- out = fluid.layers.fc(input=data,size=128,act=None)
- loss = fluid.layers.reduce_sum(out)
fluid.backward.append_backward(loss=loss)
- 添加優化相關的Op
- sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001)
sgd_optimizer.minimize(loss)
- 可以隨時調用print(fluid.default_main_program())來輸出當前的main_program
- 當構建完成整個Program后,調用下面的接口執行內存優化:
fluid.memory_optimize(fluid.default_main_program())
-
- 注:內存優化目前仍在持續開發中,有可能不夠穩定。
總結:編譯時執行流程
- 用戶定義前向計算
- 添加反向Op到default_main_program
- 添加 gradient clipping Op 到
- 添加 regularization Op 到default_main_program
- 為指定的優化算法,添加相關的狀態 variable of optimizer 到default_startup_program
- 狀態相關 variable是指如學習率, 歷史 momentum, 二階momentum等
- 添加初始化 variable 的Op 到 default_startup_program
- 為整個網絡最后一個op,添加設置其接受到的梯度的Op到default_main_program
- 進行內存優化規劃
Feed 數據 (一):通過 feed 字典
- 執行executor的run方法時,指定feed字典,feed op 會將指定的數據放到x和y兩個Variable中
- y_data = np.random.randint(0, 8, [1]).astype("int32")
- y_tensor = core.Tensor()
- y_tensor.set(y_data, place)
- x_data = np.random.uniform(0.1, 1, [11, 8]).astype("float32")
- x_tensor = core.Tensor()
- x_tensor.set(x_data, place)
- ……
- cost = exe.run(
- fluid.default_main_program(),
- feed={'x': x_tensor,
- 'y': y_tensor},
fetchlist=[avg_cost])
- 這種方法較為底層,一般用于單測中
Feed 數據 (二):使用 DataFeeder接口
- 編寫一個data_reader函數,data_reader是一個Python generator
- def demo_reader():
- def random_generator():
- yield np.random.uniform(0.1, 1, [4]), np.random.randint(0, 1, [1])
return random_generator
- 在訓練任務中使用 DataFeeder 接口
- cost = exe.run(
- fluid.default_main_program(),
- feed={'x': x_tensor,
- 'y': y_tensor},
- fetchlist=[avg_cost])
- train_reader = paddle.batch(
- paddle.reader.shuffle(demo_reader(), buf_size=500), batch_size=4)
- feeder = fluid.DataFeeder(place=place, feed_list=[x, y])
- for data in train_reader():
- cost = exe.run(
- fluid.default_main_program(),
- feed=feeder.feed(data),
fetch_list=[cost])
常見問題
- 如何使用 evaluator ? ➡
- accuracy = fluid.evaluator.Accuracy(input=predict, label=label)
- for pass_id in range(PASS_NUM):
- accuracy.reset()
- for data in train_reader():
- loss, acc = exe.run(fluid.default_main_program(),
- feed=feeder.feed(data),
- fetch_list=[avg_cost] + accuracy.metrics)
- pass_acc = accuracy.eval(exe)
- # acc 當前一個batch 的 accuracy
- # pass_acc 當前batch 的 accuracy
pass_total_acc = accuracy.eval(exe) # 整個pass的accuracy
- 如何在訓練中測試?➡
- 如何保存訓練好的模型?➡
- 如何加載訓練好的模型進行預測?➡
- 如何在同一個訓練任務中定義多個Program,并交替運行? ➡
- 如何profile?Fluid 實現了profile 工具,可以直接調用。請參考示例 ➡
謝謝