天天看點

含光800NPU開發指南(二)【晶片與軟體棧系列之----含光十八式】前言概要HanGuangRT C語言程式設計接口HanGuangRT簡單例程後記

前言

本章節介紹基于HanGuangAI軟體運作時(RunTime)的開發。這些運作時程式設計接口既可以整合到架構中,也可以用來實作推理引擎,或者直接被AI應用程式使用。現階段,他們是運作時控制使用含光NPU的唯一程式設計接口。

目前AI計算晶片的架構各異,表現在軟體接口上,就是沒有一套标準的程式設計接口。Nvidia的領頭羊地位,由其通用計算拓展到AI計算領域,但由于晶片架構之間差别太大,它的程式設計接口并不适合其他架構。況且就是Nvidia自己,也有不同層次,不同目的的程式設計接口:從底層的CUDA驅動接口,到CUDA運作庫,TensorRT,以及其他的運算庫。

我們在設計自己的程式設計接口的時候,盡量使用一些被普遍使用的名詞群組織方式,以友善開發者能快速了解和掌握含光NPU的程式設計。是以,如果你熟悉CUDA/TensorRT,會有一些熟悉的感覺。但是,由于NPU硬體架構,程式設計模型的一些獨特之處,HanGuangRT程式設計接口也有很多的自己的特性,需要開發者特别地關注和學習。

此部分作為“含光十八式”的第二式,像是經過第一式的劍舞而蓄滿力量,然後發出迅猛而淩厲的一劍。不見劍,隻見一道光閃出,劃過天空------“鷹擊”。

注:當你準備閱讀此文時,如果你沒有閱讀過《

含光800NPU程式設計模型

》,請你一定要先仔細地讀一讀程式設計模型。它能幫助你更好地了解下面的開發指南和例程。同時,本文将是一個指引,更具體的文檔在《

HanGuangAI SDK

》。如想了解更細節的資訊,請閱讀開發文檔。

概要

特性

HanGuangRT程式設計接口和其他的深度學習程式設計接口看起來有很多相同點,但也有自己的一些特點:

  • 統一的程式設計接口

HanGuangAI的程式設計接口,既包括裝置的控制,也包括運作時的上下文,執行管理,以及資源管理等,是目前調用裝置的唯一程式設計接口。驅動程式作為底層的接口,沒有直接暴露給使用者調用。

  • C語言接口

使用C語言作為接口程式設計語言,簡單易用,移植性好,高性能。其中一些接口,主要是查詢功能的接口,會經過包裝生成Python接口提供。

  • 運作前編譯(AOT)

提前使用Python接口(後續根據需要增加C接口)編譯連結好,生成執行計劃和硬體代碼,并做序列化。推理運作時反序列化然後執行。

後期會考慮需要提供運作時編譯JIT (Just in Time) 的方式,因為有一些使用場景JIT是有一些優勢的。

    • 不需要序列化和反序列化,
    • 靈活地根據運作時實體裝置和核心的實際可使用情況來編譯。不用提前準備多份配置的編譯結果。
    • 能更友善的提高NPU的使用率

當然,運作時編譯産生的延遲是需要考慮的。

  • 顯式和隐式相結合的核心分派

HanGuangRT以核心為機關做資源排程,提供靈活的裝置核心使用方式。

  • 以執行計劃作為整體來配置設定資源

把一個業務場景作為一個整體,為保證完整的場景能流暢運作,建立一個相關引擎組的執行計劃,以執行計劃作為整體來配置設定資源。

常用名詞

為友善使用者閱讀和了解,特對一些常用語進行統一簡單的解釋,後面具體章節還會對一些用語做詳細地介紹:

  • 裝置(Device/NPU):此處指的是含光800NPU(Neural-network Processing Unit),一個裝置就是一塊NPU卡,通常包括多個NPU運算核心。多個裝置之間沒有之間連接配接。同一個系統上的多個裝置是通過PCIE傳輸資料。
  • 核心(Core):NPU的一個完整的計算核心,可獨立排程運作的最小機關,核心之間通過On-Chip Bus連接配接。
  • 本地存儲(Local Memory/LM):每個核心有獨立的片上存儲(LM)。LM隻能被自己的核心通路。核間通過總線CHUB可以連接配接。
  • 模型(Model):特指深度神經網路模型。不同的架構有可能有不同的模型檔案格式。有的架構,如Mxnet,會有不止一個檔案一起表示一個模型。
  • 算子(Operator):是建構神經網絡的基本機關和必要元素,定義了輸入到輸出的映射關系。
  • 子模型(Model Segment):簡稱segment,對一個完整的網絡模型進行分割,得到的包含部分算子的子網絡。
  • 引擎(Engine):一個模型或者子模型,經過NPU軟體棧編譯之後,得到的一個用于隻能在NPU上執行的單元。對架構來說,這也是一個自定義算子,稱為含光NPU引擎算子(HanGuang Engine Op)。
  • 引擎組(Engine Group):基于含光800NPU的特性,通常多個引擎需要共享一個裝置或者部分核心。是以多個引擎需要作為一個整體來進行編譯連結。這些引擎稱為一個引擎組。
  • 引擎控制(Engine Control):用于描述一個引擎執行的控制參數,包括核心的配置設定方式,單個batch需要的核心數量,本地存儲的消耗,以及用來标明引擎的ID和名。
  • 執行規劃(Execution Scheme):執行規劃是一個引擎組的所有引擎在一個核或者多核上的執行控制。是一個引擎控制清單。每個引擎裡都帶有整個引擎組的執行規劃以友善查詢。
  • 執行上下文(Execution Context):執行上下文是由一個引擎對象建立出來的特殊的上下文,是推理運作的核心資料和狀态。 此上下文隻能用于執行該引擎。每個引擎對象現在隻支援一個執行上下文。
  • 張量(Tensor):用做NPU引擎的輸入和輸出的存儲對象。張量的存儲可以設定建立于不同的存儲中。

HanGuangRT C語言程式設計接口

總的說來,含光800NPU的推理執行主要包含以下幾個方面的接口:

  • Version Query: 軟體版本資訊。
  • Error Query: 查詢錯誤的詳細資訊。
  • Device Management: 裝置-device,核心-core的配置管理以及狀态查詢等。
  • Engine Management: 建立HanGuangAI引擎,查詢輸入輸出綁定,執行計劃等資訊,為推理執行做準備。
  • Execution Scheme: 擷取引擎所在組的執行計劃,得到本引擎的控制資訊。
  • Execution Context: 根據查詢的綁定和準備好的輸入輸出資料和緩存,標明核心配置設定的政策,執行推理運作。
  • Tensor Management: 張量對象用于管理輸入和輸出的張量,包括建立,查詢,資料的上傳和下載下傳等操作。
含光800NPU開發指南(二)【晶片與軟體棧系列之----含光十八式】前言概要HanGuangRT C語言程式設計接口HanGuangRT簡單例程後記

如上圖所示,HanGuangRT提供了兩種不同的方式來讓開發者使用NPU做推理運作。

  • 一種方式HanGuangRT負責隐式的核心分派。這是對一般的使用場景,比如單模型單裝置,開發者不需要知道負責的排程接口,直接使用引擎,綁定,選用合适的核心配置設定政策,由Runtime來負責配置設定和映射的工作。這樣,開發者不用了解太多的裝置,執行計劃等細節,隻需要使用圖上左邊的部分接口就可以了。
  • 另一種方式,是開發者使用API自己控制核心分派。這是對比較複雜的使用場景,開發者有更多的需求,對含光NPU的軟硬體比較了解的情況下,比較适合的一種開發方式。這時候,開發者需要使用大部分HanGuangRT的接口。下面具體地講解一些細節。

版本和錯誤資訊查詢

版本查詢(Version Query)用于查詢API的版本,保證不同版本之間的應用程式相容性。

錯誤查詢(Error Query)用于解析其他API傳回的RTLresult結果,用于得到錯誤的名稱和描述。

裝置管理

裝置管理API用于查詢裝置的資訊,包括裝置的數量,每個裝置上核心的數量,本地存儲的大小等;還可以顯式地擷取和管理裝置和核心。和GPU或者其他的AI晶片不大一樣的是,含光800NPU的裝置管理是以核心為基本機關的。表現在程式設計接口上,就是需要指定裝置的索引和核心的Mask。

裝置切換

多個裝置時,裝置索引從0開始編号。通過hgDeviceSetCurrent可以設定目前的裝置,後面的其他操作都是基于目前的裝置上的,包括建立的引擎,執行上下文,張量等都屬于目前的裝置,不能在其他的裝置上使用。

裝置占用

從程式設計模型一章節我們了解到,含光800NPU是将模型參數常駐本地存儲的高效方式工作。模型的上傳是一個比較費時的動作,上傳之後希望保持模型對核心的占有狀态。是以,開發者需要了解和管理裝置核心被模型引擎的占有狀态。

我們提供了兩種裝置核心的占有方式,一種是顯式地使用裝置管理API占有和釋放。一種是隐式地由引擎的執行上下文來占有和釋放。顯式占有裝置的API:

hgResult hgDeviceRetain(hgDevice device, unsigned int coreCount, 
                        hgDevicePriority priority, 
                        unsigned int* retainedCoreMask);
hgResult hgDeviceRelease(hgDevice device, unsigned int coreMask);           

裝置核心共享

為提高裝置核心使用率,有時需要多個模型/引擎共享核心。之前的章節中講述了編譯的時候如何讓多個模型共享核心,這是一種靜态的共享方式。運作的時候,這幾個模型會裝載到相關對應的實體核心上,然後在這些模型引擎釋放之前,其他的模型将不能再往這些核心上上傳。

動态核心共享,就是在運作時檢視能不能将一個引擎放在一個或多個已被占用的核心上。這種業務需求現在來看還很少,暫時不被支援。

引擎管理

引擎管理的功能包括将編譯後序列化的二進制資料反序列化生成引擎對象,查詢引擎的相關資訊,包括輸入輸出綁定和這個引擎所在的執行計劃等,并根據相關資訊來建立該引擎的執行上下文對象。輸入輸出綁定資訊在執行引擎的時候需要做對應的配置。

深度了解引擎

如何深刻地了解HanGuangAI中的引擎的概念呢?

  • 從原始網絡模型的角度看,引擎所代表的是一個網絡模型和或者是其中的一段子網絡模型,這段網絡模型是被HanGuangAI編譯器分割出來的,計劃被HanGuangAI執行的部分。
  • 從編譯後網絡模型的角度看,引擎是其中的一個節點,一個特别的被HanGuangAI支援的算子。
  • 從資料内容的角度看,引擎裡面包含了多種資訊的資料,這些資料在編譯的時候生成,在執行的時候需要使用。主要資料有:
    • NPU硬體指令;
    • 模型參數,包括權重,常量等;
    • 綁定(輸入,輸出,常量等)的描述,屬性資訊;
    • 執行計劃,引擎執行所需的一些參數。
  • 從生命周期的角度看,
    • 編譯後的引擎以序列化的二進制形式存在。
    • 運作時的引擎對象建立(hgEngineCreate)之後,反序列在系統記憶體中。
    • 引擎的執行上下文建立(hgEngineCreateExecutionContext)成功後,引擎裡的指令,模型參數裝載進NPU

引擎綁定

引擎的綁定(Bindings)指的是引擎運作的時輸入輸出張量的抽象,也就是引擎所表示的這個子網絡的輸入和輸出。針對引擎,提供了一系列的接口來擷取引擎的綁定資訊,這些資訊是準備輸入資料,準備輸入輸出張量的存儲等的必要資訊。具體的包括:

  • 綁定的個數,索引,名詞,資料次元
  • 是否是輸入,布局等資訊。

這些資訊由一系列的接口hgEngineGetBindingXxx()來提供。

其他

後面會用到的其他一些與引擎相關的接口包括:

  • hgEngineGetExecutionScheme:擷取引擎所在的引擎組執行計劃。
  • hgEngineGetEngineID:擷取引擎所在的引擎組執行計劃裡的ID。此ID可用于查詢引擎控制資訊。
  • hgEngineGetMaxBatchSize:擷取引擎所允許的最大批處理大小。可設定flags用于指定計算批處理大小的一些資訊:張量是否是在Host memory;引擎的執行上下文是不是以最大核心數來配置設定的。參見hgBatchSizeFlags的定義。

執行上下文

執行上下文是由一個引擎對象建立出來的特殊的上下文,是推理運作的核心資料和狀态。 此上下文隻能用于執行建立它的引擎。同時,每個引擎對象現在隻支援一個執行上下文。

執行上下文有兩個重要的接口,一個是建立,一個是執行。

建立

建立一個執行上下文,主要是需要給引擎的執行配置設定核心和上傳引擎的模型參數。這是一個比較重要的步驟。如果建立失敗,則說明配置設定不成功,需要重新考慮如何配置設定資源,或者等待資源釋放以後再使用。根據優先級做逐出(eviction)的機制還沒有實作。

核心配置設定也是有顯式的和隐式的兩種,可以由hgCoreAllocPolicy來指定。這兩種方式有很大的差別:

  • 隐式的,是根據整個執行組(scheme)所需要的核心為基本機關,使用HG_CORE_ALLOC_MINIMUM或者HG_CORE_ALLOC_MAXIMUM。前者是隻配置設定一個執行組所需的核心,後者是盡可能的多配置設定核心,也就是1~N倍一個執行組所需的核心。
  • 顯式的,是由開發者直接指定目前單個引擎所需要的核心,開發者必須通過下一節的執行控制來擷取相關資訊,同時獲得裝置資訊,然後手動給個引擎配置設定好核心,必須保證配置設定正确,否則會發生分不成功的問題。這一模式對開發者要求比較高。需要深刻地了解相關的資訊和設定方式。

建立執行上下文的接口是:

hgResult hgEngineCreateExecutionContext(hgEngine engine, hgExecutionContext* context,     
                                        hgCoreAllocPolicy policy, uint32_t coreMask);           

執行

同步執行

目前,HanGuangAI運作時隻提供了一種同步執行的機制。也就是執行函數調用會等待執行完成,得到輸出之後才會傳回。異步的enqueue機制還沒有加入到執行接口中。

由于沒有異步的執行方式,是以為了達到更高的吞吐,開發者可以一次送入大批量的資料,批量大小以batchSize設定。最大的batchSize可以由接口hgEngineGetMaxBatchSize查詢得到。

執行之前,還需要把輸入和輸出的資料綁定設定好。綁定可以是資料的系統存儲(System Memory)的位址,也可以是建立在Locked Host Memory的張量對象句柄。後面在張量對象裡面介紹兩種差別。hgHostTensorDesc用于配置一些Host張量的資訊,用以幫助解析這些張量的資料。

接口定義為:

hgResult hgExecutionContextExecute(hgExecutionContext context, int batchSize, 
                                   void** bindings, const hgHostTensorDesc* tensorInfo);           

多批量執行

含光800NPU計算核心算力強勁,在多數情況下,使用更大的批量輸入,能夠更高效的利用計算核心,提高整個NPU的計算吞吐。

特别的,HanGuangRT對多批量的執行進行了優化,在上傳,運算,下載下傳三個步驟之間做多線程異步處理,形成了更好的流處理工作模式。是以在多數追求吞吐量的使用場景裡,應該使用更大數目的“batchSize”批量執行。

執行計劃

執行計劃(Execution Scheme)是HanGuangAI所提供的一個特别的概念。它是為多引擎裝載而生。具體地說,它是為了靜态地給多個引擎配置設定核心和裝載模型參數,進而保證整個業務流程能順利地在指定的裝置核心上運作。是以當一個引擎運作的時候,會将同一個scheme裡的其他引擎所需的核心也一起配置設定好。但不論基于架構還是獨立的推理引擎,引擎都是一個一個獨立運作的,為了能一起配置設定資源,需要每個引擎裡必須有整個執行組的所有資訊。

前面提到,對執行組所需要的資源,我們提供了兩種擷取方式,一種是顯式的,一種是隐式的。這裡,如果需要顯式地

程式設計接口

從一個引擎裡獲得它所屬的執行引擎的接口是:

hgResult hgEngineGetExecutionScheme(hgEngine engine, hgExecutionScheme* scheme);           

然後,可以檢視執行組裡一共有多少個引擎:

hgResult hgSchemeGetEngineCount(hgExecutionScheme scheme, int* count);           

其中,每個引擎的ID可以由下面的接口從引擎中獲得:

hgResult hgEngineGetEngineID(hgEngine engine, int* engineId);           

最後,可以從執行計劃裡讀取每個引擎的執行控制(Engine Control):

hgResult hgSchemeGetEngineControl(hgExecutionScheme scheme, int engineId, 
                                  hgEngineControl** engControl);           

資料結構

每個引擎的執行資訊,定義為hgEngineControl,它包括的資訊如下:

typedef struct hgEngineControl
{
    /// engine index in the engine group
    int                 engineId;
    /// name string of the engine
    std::string         engineName;
    /// virtual device to put the engine on,
    /// engines with same virtual device id share the same device
    int                 virtualDeviceId;
    /// engines with same group ID can share memory
    int                 engineGroupId;
    /// execution mode :normal, weight_split,
    /// engines with same virtual device id should have same execute mode
    hgExecuteMode       executeMode;
    /// the minimum core number for execution of one batch
    int                 coresPerBatch;
    /// the virtual core mask on the virtual device
    int                 coreMask;
    /// local memory consumption of the engine for one patch
    size_t              memoryConsumption;
}hgEngineControl;           

虛拟裝置核心

在執行計劃和引擎控制中,使用的裝置ID,核心Mask,都是以編譯的時候提供的裝置數和核心數作為虛拟裝置和核心做的編号。裝置ID從0開始做索引,核心Mask也是從bit0開始做索引。

在隐式配置設定裝置核心的時候,HanGuangRT會内部生成引擎的Scheme對象,根據相關的資訊做核心配置設定以及虛拟核心和實體核心之間的映射(mapping)。如果開發者想要顯式地配置設定,可以通過相關的API查詢實體核心和執行計劃資訊,做好映射關系,然後通過對應的API來設定。

張量管理

張量(Tensor),用做NPU引擎的輸入,輸出的存儲對象。張量對象的操作包括根據描述建立對象,查詢描述和大小屬性,map/unmap可用于使用者自己上傳和下載下傳資料。也可以使用接口交由運作時來上傳/下載下傳資料。

輸入資料的上傳和輸出資料的下載下傳有兩種方式:

  1. 将資料放在System Memory,将位址作為binding指針傳入,同時使用System Memory指針作為接收輸出的binding。這種方式下,Runtime會負責資料在System Memory和NPU Host Memory之間的傳輸。在這種工作方式中,上層應用不需要使用張量接口也可以執行推理。接口上比較簡單友善。
  2. 使用張量接口,建立Host Memory的張量對象,上層應用将資料直接拷貝到Map出來的空間裡。當然拷貝的時候需要主要資料的對齊。對齊後的大小可以查詢獲得。在有些情況下,後面這種方式可以減少一次拷貝,對性能和帶寬有幫助。

Tensor由下面API建立。

typedef enum hgMemFlags
 {
     HG_MEM_SYSTEM    = 1 << 0,  // CPU memory
     HG_MEM_HOST      = 1 << 1,  // page locked host memory
 }hgMemFlags;
 
 hgResult hgTensorCreate(const hgTensorDesc* tensorDesc, 
                         const hgMemDesc * memDesc, 
                         unsigned int flags, 
                         hgTensor *handle);           

其它幾個比較重要的API:

/// If the tensor hasn't be created with desc, this call will set and allocate mthe underlying
/// meemory for the tensor. If the tensor has already be set with desc, the call will update 
/// the desc and create the tensor memory if necessary 
hgResult hgTensorUpdateDesc(hgTensor handle, const hgTensorDesc* desc);

/// Uploads the \p data into the tensor object
hgResult hgTensorData(hgTensor handle, const void *data, size_t size);

/// Map the tensor to get CPU access address 
hgResult hgTensorMap(hgTensor handle, void **data);

/// Unmap the tensor to complete access
hgResult hgTensorUnmap(hgTensor handle);           

更多的Tensor使用請參考文檔。 

HanGuangRT簡單例程

本例程主要介紹怎麼使用HanGuangRT提供的API在NPU上做推理。在做推理之前,使用者需要準備好:

  1. 使用HanGuangAI量化編譯得到的序列化的EngineOp
  2. 此EngineOp對應的輸入輸出的資料和相關資訊

頭檔案

#include "alinpu_hgrt_c_api_pub.h"           

查詢NPU裝置資訊

首先,查詢runtime的版本,需要和之前編譯EngineOp使用的HanGuangAI是同一個版本。可以根據釋出的對應版本的文檔知道相應的功能,API的情況,避免版本不對導緻錯誤的使用

int runtimeVersion;
hgRuntimeGetVersion(&runtimeVersion);           

可以查詢目前系統裡Device的情況,包括有幾個NPU,每個NPU的核心的數目,和相應的LM的大小。顯式指派核心的時候需要相關資訊。

int count = 0;
hgDeviceGetCount(&count);
CHECK(count > 0);
hgDevice original;
hgDeviceGetCurrent(&original);
int cores;
hgDeviceGetAttribute(original, HG_DEVICE_CORE_COUNT, &cores);
...           

顯式擷取裝置核心

下面的API可以顯式地擷取和釋放裝置核心,如果沒有複雜的使用場景,推薦使用者使用隐式的機制,應用程式不用顯式地擷取裝置核心。

uint retainedCoreMask = 0;
int cores = 2;
hgDeviceRetain(device, cores, HG_DEVICE_P0, &retainedCoreMask);
...
if (retainedCoreMask > 0) {
  hgDeviceRelease(device, retainedCoreMask);
}           

建立引擎

使用編譯後序列化的引擎資料建立引擎。獨立運作時,不用指定架構。

hgEngineCreate(serialized_engine_.c_str(), 
               serialized_engine_.size(),
               &alinpu_engine_ptr_);           

查詢執行計劃資訊

從引擎裡查詢單個執行計劃裡的引擎的資訊,以幫助標明裝置的核心。

int engine_id = 0;
 hgEngineGetEngineID(alinpu_engine_ptr_, &engine_id);
 hgExecutionScheme scheme;
 hgEngineGetExecutionScheme(alinpu_engine_ptr_, &scheme);
 int engine_count = 0;
 hgSchemeGetEngineCount(scheme, &engine_count);
 for (int i = 0; i < engine_count; i++) {
     hgEngineControl* control;
     hgSchemeGetEngineControl(scheme, i, &control);
}           

hgEngineControl的詳細資訊見API參考手冊。當一個執行計劃裡有多個引擎的時候,執行計劃裡包含了引擎和裝置核心的對應關系。查詢執行計劃資訊是執行推理的必要步驟。

建立執行上下文

根據查詢得到的執行計劃資訊,建立執行計劃上下文。

// try create alinpu context.
hgEngineCreateExecutionContext(alinpu_engine_ptr_,
                               &alinpu_execution_context_ptr_,
                               HG_CORE_ALLOC_MINIMUM, 0);
// if create context fail, exit the function.
if (alinpu_execution_context_ptr_ == nullptr) {
  LOG(ERROR) << "Create Execution Context failed!\n";
  return false;
}           

配合擷取裝置核心的兩種方式,建立執行上下文的時候,也有兩類對應的政策

hgCoreAllocPolicy

:

  • 隐式的政策,包括

    HG_CORE_ALLOC_MINIMUM

    , 

    HG_CORE_ALLOC_MAXIMUM

    ,前者提供最小的配置設定,一次一個批次(batch)。而後者是高性能模型,将為此執行計劃裡的引擎配置設定盡可能多的核心,以保證最高的性能和吞吐。
  • 如果核心是顯式的擷取,可以使用

    HG_CORE_ALLOC_AS_CORE_MASK

    來指定合适的實體核心給此引擎。

準備引擎輸入輸出

首先查詢所有輸入和輸出綁定的屬性。

// input/output bindings
int32_t num_binding_;  
hgEngineGetNbBindings(alinpu_engine_ptr_, &num_binding_);
buffers_.resize(num_binding_);
for (int i = 0; i < num_binding_; i++) {
  hgEngineGetBindingAttribute(alinpu_engine_ptr_, i, 
                              HG_BINDING_ATTR_IS_INPUT, 
                              &is_input_[i]);
  ...
}           

根據上面的資訊,設定和配置設定輸入輸出的buffer。

同步執行,得到輸出資料

最後,使用上面得到的批量大小

num_batch

,綁定緩存

buffers_

,和Host張量的資訊

hinfo_

執行引擎。

// execute alinpu engine op.
hgResult ret = hgExecutionContextExecute(alinpu_execution_context_ptr_, 
                                         num_batch, &buffers[0], &hdesc);           

後記

HanGuangRT還處于繼續開發當中,一方面,新的功能接口會陸續地加進來,另一方面,原有的接口可能也會有一些改動,請關注我們最新的文檔,它會随着軟體棧的釋出一起更新。你有任何疑議或建議,請聯系我或者請發送郵件到:

[email protected]