用 Ascend CL 从零写一个推理程序

前言

用 PyTorch 推理很简单,但生产环境里经常需要更底层的控制------比如 C++ 服务、嵌入式设备、或者极致的性能优化。这时候就要用 Ascend CL(Compute Language)直接调用 NPU。

下面从零写一个完整的推理程序,包含模型加载、输入预处理、推理、输出后处理。


一、初始化 Ascend CL 环境

任何 Ascend CL 程序的第一步:初始化运行时。

cpp 复制代码
#include <acl/acl.h>
#include <iostream>

int main() {
    // 1. 初始化 ACL 运行时
    aclError ret = aclInit(nullptr);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclInit failed: " << ret << std::endl;
        return -1;
    }
    
    // 2. 设置使用的设备
    int deviceId = 0;
    ret = aclrtSetDevice(deviceId);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclrtSetDevice failed: " << ret << std::endl;
        aclFinalize();
        return -1;
    }
    
    // 3. 创建 Context 和 Stream
    aclrtContext context;
    aclrtStream stream;
    ret = aclrtCreateContext(&context, deviceId);
    ret = aclrtCreateStream(&stream);
    
    std::cout << "Ascend CL initialized successfully" << std::endl;
    
    // ... 推理代码 ...
    
    // 4. 释放资源
    aclrtDestroyStream(stream);
    aclrtDestroyContext(context);
    aclrtResetDevice(deviceId);
    aclFinalize();
    
    return 0;
}

编译命令:

bash 复制代码
g++ -o inference inference.cpp \
    -I${ASCEND_HOME}/acl/include \
    -L${ASCEND_HOME}/acl/lib64 \
    -lacl

二、加载 .om 模型

模型加载分三步:加载文件、获取模型描述、创建推理句柄。

cpp 复制代码
// 加载模型文件
aclmdlDesc* modelDesc = nullptr;
aclmdlHandle modelHandle = nullptr;

int loadModelFromFile(const char* modelPath) {
    // 1. 加载 .om 文件
    uint32_t modelId = 0;
    aclError ret = aclmdlLoadFromFile(modelPath, &modelId);
    if (ret != ACL_SUCCESS) {
        std::cerr << "Load model failed: " << ret << std::endl;
        return -1;
    }
    
    // 2. 获取模型描述
    modelDesc = aclmdlCreateDesc();
    ret = aclmdlGetDesc(modelDesc, modelId);
    if (ret != ACL_SUCCESS) {
        std::cerr << "Get model desc failed: " << ret << std::endl;
        aclmdlUnload(modelId);
        return -1;
    }
    
    // 3. 打印模型输入输出信息
    size_t numInputs = aclmdlGetNumInputs(modelDesc);
    size_t numOutputs = aclmdlGetNumOutputs(modelDesc);
    std::cout << "Model inputs: " << numInputs << ", outputs: " << numOutputs << std::endl;
    
    for (size_t i = 0; i < numInputs; i++) {
        aclmdlIODims dims;
        aclmdlGetInputDims(modelDesc, i, &dims);
        std::cout << "Input " << i << " shape: [";
        for (size_t j = 0; j < dims.dimCount; j++) {
            std::cout << dims.dims[j];
            if (j < dims.dimCount - 1) std::cout << ", ";
        }
        std::cout << "]" << std::endl;
    }
    
    return modelId;
}

三、准备输入数据

输入数据需要从 CPU 拷贝到 NPU 的 Device 内存。

cpp 复制代码
// 创建输入 Dataset
aclmdlDataset* createInputDataset(void* hostData, size_t dataSize) {
    // 1. 创建 Dataset
    aclmdlDataset* dataset = aclmdlCreateDataset();
    
    // 2. 分配 Device 内存
    void* deviceBuffer = nullptr;
    aclError ret = aclrtMalloc(&deviceBuffer, dataSize, ACL_MEM_MALLOC_HUGE_FIRST);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclrtMalloc failed" << std::endl;
        return nullptr;
    }
    
    // 3. 拷贝数据从 Host 到 Device
    ret = aclrtMemcpy(deviceBuffer, dataSize, hostData, dataSize, ACL_MEMCPY_HOST_TO_DEVICE);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclrtMemcpy failed" << std::endl;
        aclrtFree(deviceBuffer);
        return nullptr;
    }
    
    // 4. 创建 DataBuffer 并添加到 Dataset
    aclDataBuffer* dataBuffer = aclCreateDataBuffer(deviceBuffer, dataSize);
    aclmdlAddDatasetBuffer(dataset, dataBuffer);
    
    return dataset;
}

预处理示例:图像归一化

cpp 复制代码
// 对图像数据做归一化:mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
void preprocessImage(float* input, const uint8_t* image, int height, int width) {
    const float mean[3] = {0.485f, 0.456f, 0.406f};
    const float std[3] = {0.229f, 0.224f, 0.225f};
    
    for (int c = 0; c < 3; c++) {
        for (int h = 0; h < height; h++) {
            for (int w = 0; w < width; w++) {
                int idx = c * height * width + h * width + w;
                int imgIdx = h * width * 3 + w * 3 + c;
                input[idx] = (image[imgIdx] / 255.0f - mean[c]) / std[c];
            }
        }
    }
}

四、执行推理

推理是异步的,需要用 Stream 同步。

cpp 复制代码
// 执行推理
int executeInference(uint32_t modelId, aclmdlDataset* input, aclmdlDataset* output, aclrtStream stream) {
    // 1. 执行推理(异步)
    aclError ret = aclmdlExecuteAsync(modelId, input, output, stream);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclmdlExecuteAsync failed: " << ret << std::endl;
        return -1;
    }
    
    // 2. 同步等待推理完成
    ret = aclrtSynchronizeStream(stream);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclrtSynchronizeStream failed: " << ret << std::endl;
        return -1;
    }
    
    std::cout << "Inference completed" << std::endl;
    return 0;
}

五、获取输出结果

输出数据在 NPU 的 Device 内存,需要拷贝回 CPU 才能读取。

cpp 复制代码
// 获取输出数据
int getOutputData(aclmdlDataset* outputDataset, float* hostOutput, size_t outputSize) {
    // 1. 获取输出 DataBuffer
    aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(outputDataset, 0);
    void* deviceBuffer = aclGetDataBufferAddr(dataBuffer);
    size_t bufferSize = aclGetDataBufferSizeV2(dataBuffer);
    
    // 2. 拷贝数据从 Device 到 Host
    aclError ret = aclrtMemcpy(hostOutput, outputSize, deviceBuffer, bufferSize, ACL_MEMCPY_DEVICE_TO_HOST);
    if (ret != ACL_SUCCESS) {
        std::cerr << "aclrtMemcpy device to host failed" << std::endl;
        return -1;
    }
    
    return 0;
}

后处理示例:Softmax + Top-K

cpp 复制代码
#include <algorithm>
#include <vector>

void softmax(float* data, int size) {
    float maxVal = *std::max_element(data, data + size);
    float sum = 0.0f;
    for (int i = 0; i < size; i++) {
        data[i] = expf(data[i] - maxVal);
        sum += data[i];
    }
    for (int i = 0; i < size; i++) {
        data[i] /= sum;
    }
}

std::vector<int> topK(float* probs, int size, int k) {
    std::vector<std::pair<float, int>> pairs;
    for (int i = 0; i < size; i++) {
        pairs.push_back({probs[i], i});
    }
    std::partial_sort(pairs.begin(), pairs.begin() + k, pairs.end(), 
                      [](auto& a, auto& b) { return a.first > b.first; });
    
    std::vector<int> indices;
    for (int i = 0; i < k; i++) {
        indices.push_back(pairs[i].second);
    }
    return indices;
}

六、完整推理流程

cpp 复制代码
int main() {
    // 初始化
    aclInit(nullptr);
    aclrtSetDevice(0);
    aclrtContext context;
    aclrtStream stream;
    aclrtCreateContext(&context, 0);
    aclrtCreateStream(&stream);
    
    // 加载模型
    uint32_t modelId = loadModelFromFile("resnet50.om");
    
    // 准备输入
    float inputData[3 * 224 * 224];
    uint8_t imageData[224 * 224 * 3];
    preprocessImage(inputData, imageData, 224, 224);
    
    aclmdlDataset* input = createInputDataset(inputData, sizeof(inputData));
    
    // 准备输出
    aclmdlDataset* output = aclmdlCreateDataset();
    void* outputBuffer;
    aclrtMalloc(&outputBuffer, 1000 * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST);
    aclDataBuffer* outputData = aclCreateDataBuffer(outputBuffer, 1000 * sizeof(float));
    aclmdlAddDatasetBuffer(output, outputData);
    
    // 执行推理
    executeInference(modelId, input, output, stream);
    
    // 获取输出
    float outputProb[1000];
    getOutputData(output, outputProb, sizeof(outputProb));
    
    // 后处理
    softmax(outputProb, 1000);
    auto top5 = topK(outputProb, 1000, 5);
    
    std::cout << "Top 5 classes: ";
    for (int idx : top5) {
        std::cout << idx << " (" << outputProb[idx] << ") ";
    }
    std::cout << std::endl;
    
    // 释放资源
    aclmdlUnload(modelId);
    aclrtDestroyStream(stream);
    aclrtDestroyContext(context);
    aclrtResetDevice(0);
    aclFinalize();
    
    return 0;
}

七、性能优化技巧

1. 批量推理

单次推理的开销包含模型加载、内存分配、Stream 同步等。批量推理可以分摊这些固定开销。

cpp 复制代码
// batch_size = 8
float inputData[8 * 3 * 224 * 224];
aclmdlDataset* input = createInputDataset(inputData, sizeof(inputData));

2. 输入 Buffer 复用

多次推理时,Device 内存可以复用,不用每次分配。

cpp 复制代码
// 提前分配,多次使用
void* inputBuffer = nullptr;
aclrtMalloc(&inputBuffer, inputSize, ACL_MEM_MALLOC_HUGE_FIRST);

// 第一次推理
aclrtMemcpy(inputBuffer, inputSize, data1, inputSize, ACL_MEMCPY_HOST_TO_DEVICE);
aclmdlExecuteAsync(modelId, input, output, stream);

// 第二次推理(复用同一块内存)
aclrtMemcpy(inputBuffer, inputSize, data2, inputSize, ACL_MEMCPY_HOST_TO_DEVICE);
aclmdlExecuteAsync(modelId, input, output, stream);

3. 异步流水线

预处理、推理、后处理用不同的 Stream 并行。

cpp 复制代码
aclrtStream preprocessStream, inferenceStream, postprocessStream;
aclrtCreateStream(&preprocessStream);
aclrtCreateStream(&inferenceStream);
aclrtCreateStream(&postprocessStream);

// 流水线:第一批预处理、第二批推理、第三批后处理同时进行

参考资源


总结

用 Ascend CL 写推理程序的核心流程:初始化运行时 → 加载 .om 模型 → 准备输入 Dataset → 执行推理 → 获取输出 Dataset。每个步骤都有对应的 API,理解了内存模型(Host vs Device)之后就不难。性能优化集中在三点:批量推理分摊固定开销、Device 内存复用减少分配次数、异步流水线让预处理/推理/后处理并行。生产环境里用 C++ 直接调用 ACL,比 PyTorch 更灵活,延迟也更可控。

相关推荐
@蔓蔓喜欢你8 小时前
技术博客写作:分享知识,提升影响力
人工智能·ai
zxsz_com_cn8 小时前
设备预测性维护实施案例解析
人工智能
Loli_Wolf8 小时前
AI 原生研发闭环:从提需到线上监测,再自动回到提需
人工智能·深度学习·算法·microsoft·ai·ai编程·harness
kishu_iOS&AI9 小时前
NLP —— Transformers库使用
人工智能·自然语言处理·迁移学习
南屹川9 小时前
【中间件】RabbitMQ消息队列实战:从入门到精通
人工智能
ftpeak9 小时前
AI开发~OpenAI专家之路:构建企业级AI应用(第三部分·上)
人工智能·ai·ai编程
MediaTea9 小时前
DL:Transformer 的基本原理与 PyTorch 实现
人工智能·pytorch·python·深度学习·transformer
wuxinyan1239 小时前
工业级大模型学习之路024:LangChain零基础入门教程(第七篇):RAG 系统评估、全链路调优
人工智能·python·学习·langchain
koharu1239 小时前
CrewAI :多智能体开发
人工智能·llm·agent·crewai