一本精品热在线视频,久久免费视频分类,精品婷婷乱码久久久久久蜜桃,在线可以免费观看的Av

<mark id="vd61v"><dl id="vd61v"></dl></mark>
    <sub id="vd61v"><ol id="vd61v"></ol></sub>

  • <sub id="vd61v"><ol id="vd61v"></ol></sub>
    "); //-->

    博客專欄

    EEPW首頁 > 博客 > C++部署的性能優(yōu)化方法

    C++部署的性能優(yōu)化方法

    發(fā)布人:地平線開發(fā)者 時間:2025-04-28 來源:工程師 發(fā)布文章

    一、使用結(jié)構(gòu)體提前存放常用變量

    在編寫前后處理函數(shù)時,通常會多次用到一些變量,比如模型輸入 tensor 的 shape,count 等等,若在每個處理函數(shù)中都重復(fù)計算一次,會增加部署時的計算量。對于這種情況,可以考慮使用結(jié)構(gòu)體,并定義一個初始化函數(shù)。先計算好需要的值,之后需要用到該變量的時候直接引用(&)傳遞即可。


    // 定義結(jié)構(gòu)體
    struct ModelInfo {
       hbDNNPackedHandle_t packed_handle;
       hbDNNHandle_t       model_handle;
       const char *        model_path;
       const char **       model_name_list;
       int model_count;
       int input_count;
       int output_count;
    };
    // 函數(shù)聲明
    int init_model(ModelInfo &model_info);
    int other_function(ModelInfo &model_info, ...);
    //主函數(shù)
    int main(){
       // 初始化
       ModelInfo prefill_model = {0};
       prefill_model.model_path = drobotics_model_path_prefill.c_str();
       init_model(prefill_model);
       // 在其他函數(shù)中使用引用傳遞相關(guān)參數(shù)
       other_function(prefill_model, ...);
       return 0;
    }
    // 初始化函數(shù)的完整定義
    int init_model(ModelInfo &model_info) {
       hbDNNInitializeFromFiles(&model_info.packed_handle, &model_info.model_path, 1);
       HB_CHECK_SUCCESS(hbDNNGetModelNameList(&model_info.model_name_list, &model_info.model_count, model_info.packed_handle),
               "hbDNNGetModelNameList failed");
       HB_CHECK_SUCCESS(hbDNNGetModelHandle(&model_info.model_handle, model_info.packed_handle, model_info.model_name_list[0]),
           "hbDNNGetModelHandle failed");
       HB_CHECK_SUCCESS(hbDNNGetInputCount(&model_info.input_count, model_info.model_handle), "hbDNNGetInputCount failed");
       HB_CHECK_SUCCESS(hbDNNGetOutputCount(&model_info.output_count, model_info.model_handle), "hbDNNGetOutputCount failed");
       return 0;
    }
    // 其他函數(shù)參數(shù)中使用引用傳遞
    int other_function(ModelInfo &model_info, ...){
       ...
    }


    二、函數(shù)使用引用代替值傳遞

    考慮到 C++的特性,函數(shù)的參數(shù)建議使用引用 (&) 來代替值傳遞,有這幾個顯著優(yōu)點:

      只將原對象的引用傳遞給函數(shù),避免不必要的拷貝,降低計算耗時

      因為不會復(fù)制數(shù)據(jù),所以引用相比值傳遞可以避免內(nèi)存的重復(fù)開銷,降低內(nèi)存占用

    但需要注意,引用會允許函數(shù)修改原始數(shù)據(jù),因此若不希望原始數(shù)據(jù)被修改,請不要使用引用方法。



    三、量化/反量化融合

    3.1 在前后處理的循環(huán)中融合

    在前后處理中通常會遍歷數(shù)據(jù),而量化/反量化也會遍歷數(shù)據(jù),因此可以考慮合并計算,以減少數(shù)據(jù)遍歷耗時。這是最常見的量化/反量化融合思路,可以直接參考 ai benchmark 中的大量源碼示例。

    3.2 將數(shù)據(jù)存進 tensor 時融合

    如果在前處理中沒找到融合的機會,那么也可以在數(shù)據(jù)復(fù)制進 input tensor 的時候做量化計算。

    int64_t kv_count = 0;
    int8_t* input_ptr = reinterpret_cast<int8_t*>(model_info.input_tensors[i].sysMem.virAddr);
    for (int n = 0; n < total_count; n++) {
       input_ptr[n] = quantize_int8(kv_decode[kv_count++], cur_scale, cur_zero_point);
    }


    3.3 填充初始值時,提前計算量化后的值

    有時我們想給模型準備特定的輸入,比如生成一個全 0 數(shù)組,再為數(shù)組的特定區(qū)域填充某個固定的浮點值。在這種情況下,如果先生成完整的浮點數(shù)組,再遍歷整個數(shù)組做量化,會產(chǎn)生不必要的遍歷耗時,常見的優(yōu)化思路是先提前計算好填充值量化后的結(jié)果,填充的時候直接填入定點值,這樣就可以避免多余的量化耗時。

    std::vector<int16_t> prepare_decode_attention_mask(ModelInfo &model_info,
       DecodeInfo &decode_info, PrefillInfo &prefill_info, int decode_infer_num){

       // 初始化全 0 數(shù)組

       std::vector<int16_t> decode_attention_mask_int(decode_info.kv_cache_len, 0);
       // 提前計算填充值量化后的結(jié)果
       hbDNNQuantiScale scale = model_info.input_tensors[1].properties.scale;
       auto cur_scale = scale.scaleData[0];
       auto cur_zero_point = scale.zeroPointData[0];
       int16_t pad_value_int = quantize_s16(-2048.0, cur_scale, cur_zero_point);
       // 將量化后的填充值填充到數(shù)組中特定區(qū)域
       for(int i = 0; i < decode_info.kv_cache_len - prefill_info.tokens_len
           - decode_infer_num -1; i++){
           decode_attention_mask_int[i] = pad_value_int;
       }

       // 返回相當于已經(jīng)量化了的數(shù)組

       return decode_attention_mask_int;
    }


    3.4 根據(jù)后處理的實際作用,跳過反量化

    在某些情況下,比如后處理只做 argmax 時,完全沒有必要做反量化,直接使用整型數(shù)據(jù)做 argmax 即可。需要用戶根據(jù)后處理的具體原理來判斷是否使用這種優(yōu)化方法。

    // 直接對模型輸出的 int16_t 數(shù)據(jù)做 argmax 計算
    int logits_argmax(std::vector<hbDNNTensor> &output_tensor) {
       auto data_tensor = reinterpret_cast<int16_t *>(output_tensor[0].sysMem.virAddr);
       int maxIndex = -1;
       int maxValue = -32768;
       for (int i = 0; i < 151936; ++i) {
           if (data_tensor[i] > maxValue) {
               maxValue = data_tensor[i];
               maxIndex = i;
           }
       }
       return maxIndex;
    }


    四、循環(huán)推理同個模型時,輸出數(shù)據(jù)直接存進輸入 tensor

    在某些情況下,我們希望 C++程序能重復(fù)推理同一個模型,并且模型上一幀的輸出可以作為下一幀的輸入。如果按照常規(guī)手段,我們可能會將輸出 tensor 的內(nèi)容保存到特定數(shù)組,再把這個數(shù)組拷貝到輸入 tensor,這樣一來一回就產(chǎn)生了兩次數(shù)據(jù)拷貝的耗時,也占用了更多內(nèi)存。實際上,我們可以將模型的輸出 tensor 地址直接指向輸入 tensor,這樣模型第一幀的推理結(jié)果會直接寫在輸入 tensor 上,推理第二幀的時候就可以直接利用這份數(shù)據(jù),不需要再單獨準備輸入,可以節(jié)省大量耗時。

    如果想使用該方法,需要模型輸入輸出對應(yīng)節(jié)點的 shape/stride 等信息完全相同。此外,如果模型刪除了量化/反量化算子,并且對應(yīng)的 scale 完全相同,那么重復(fù)利用的這部分 tensor 是不需要 flush 的(因為不涉及 CPU 操作),還可進一步節(jié)約耗時。

    這里舉個例子詳細說明一下。

    假設(shè)我們有一個模型,這個模型有 59 個輸入節(jié)點(0-58),57 個輸出節(jié)點(0-56),量化/反量化算子均已刪除,且輸入輸出最后 56 個節(jié)點對應(yīng)的 scale/shape/stride 等信息均相同。在第一幀推理完成后,輸出節(jié)點 1-56 的值需要傳遞給輸入節(jié)點的 3-58,那么我們在分配模型輸入輸出 tensor 的時候,輸出 tensor 只需要為 1 分配即可,在分配輸入 tensor 時,3-58 的 tensor 可以同時 push_back 給輸出 tensor。具體來說,可以這樣寫:


    int prepare_tensor(std::vector<hbDNNTensor> & input_tensor, std::vector<hbDNNTensor> & output_tensor,
                      hbDNNHandle_t dnn_handle) {
       int input_count  = 0;
       int output_count = 0;
       hbDNNGetInputCount(&input_count, dnn_handle);
       hbDNNGetOutputCount(&output_count, dnn_handle);
       for (int i = 0; i < 1; i++) {
           hbDNNTensor output;
           HB_CHECK_SUCCESS(hbDNNGetOutputTensorProperties(&output.properties, dnn_handle, i),
                            "hbDNNGetOutputTensorProperties failed");
           int output_memSize = output.properties.alignedByteSize;
           HB_CHECK_SUCCESS(hbUCPMallocCached(&output.sysMem, output_memSize, 0), "hbUCPMallocCached failed");
           output_tensor.push_back(output);
       }

       for (int i = 0; i < input_count; i++) {
           hbDNNTensor input;
           HB_CHECK_SUCCESS(hbDNNGetInputTensorProperties(&input.properties, dnn_handle, i),
                            "hbDNNGetInputTensorProperties failed");
           int input_memSize = input.properties.alignedByteSize;
           HB_CHECK_SUCCESS(hbUCPMallocCached(&input.sysMem, input_memSize, 0), "hbUCPMallocCached failed");
           input_tensor.push_back(input);
           if(i > 2){
               output_tensor.push_back(input);
           }
       }
       return 0;
    }


    在模型推理時,重復(fù)利用的這部分 tensor 不需要再 flush,因此只需要給 output_tensor 的 0,以及 input_tensor 的 0/1/2 進行 flush 操作即可(這幾個 tensor 和 CPU 產(chǎn)生了交互)。


    while(1){
       hbUCPTaskHandle_t task_handle_decode{nullptr};
       hbDNNTensor *output_decode = decode_model.output_tensors.data();
       HB_CHECK_SUCCESS(hbDNNInferV2(&task_handle_decode, output_decode,
           decode_model.input_tensors.data(), decode_model.model_handle), "hbDNNInferV2 failed");
       hbUCPSchedParam ctrl_param_decode;
       HB_UCP_INITIALIZE_SCHED_PARAM(&ctrl_param_decode);
       ctrl_param_decode.backend = HB_UCP_BPU_CORE_ANY;
       HB_CHECK_SUCCESS(hbUCPSubmitTask(task_handle_decode, &ctrl_param_decode), "hbUCPSubmitTask failed");
       HB_CHECK_SUCCESS(hbUCPWaitTaskDone(task_handle_decode, 0), "hbUCPWaitTaskDone failed");
       // 只刷新一部分輸出內(nèi)存(output_tensor 0)
       hbUCPMemFlush(&decode_model.output_tensors[0].sysMem, HB_SYS_MEM_CACHE_INVALIDATE);
       HB_CHECK_SUCCESS(hbUCPReleaseTask(task_handle_decode), "hbUCPReleaseTask failed");
       // 后處理(只針對 output_tensor 0)
       decode_argmax_id = logits_argmax(decode_model.output_tensors);
       // 準備下一幀推理的 input_tensor 0/1/2 輸入數(shù)據(jù)
       prepare_input_tensor(...);
       // 只刷新一部分輸入內(nèi)存(input_tensor 0/1/2)
       for (int i = 0; i < 3; i++) {
           hbUCPMemFlush(&decode_model.input_tensors[i].sysMem, HB_SYS_MEM_CACHE_CLEAN);
       }


    此外,如果使用了這種優(yōu)化方法,那么在模型推理結(jié)束釋放內(nèi)存時,要避免同一塊內(nèi)存的重復(fù)釋放。對于該案例,input_tensor 全部釋放完畢后,output_tensor 只需要釋放 output_tensor 0。


    for (int i = 0; i < decode_model.input_count; i++) {
       HB_CHECK_SUCCESS(hbUCPFree(&(decode_model.input_tensors[i].sysMem)), "hbUCPFree decode_model.input_tensors failed");
    }
    for (int i = 0; i < 1; i++) {
       HB_CHECK_SUCCESS(hbUCPFree(&(decode_model.output_tensors[i].sysMem)), "hbUCPFree decode_model.output_tensors failed");
    }



    五、多線程后處理

    對于 yolo v5 這種有三個輸出頭的模型,可以考慮使用三個線程同時對三個輸出頭做后處理,以顯著提升性能。


    *博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。




    相關(guān)推薦

    技術(shù)專區(qū)

    關(guān)閉