Files
2026-07-13 13:33:03 +08:00

666 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session API使用
## 创建会话
### 概述
使用MNN推理时,有两个层级的抽象,分别是`解释器Interpreter``会话Session``Interpreter`是模型数据的持有者;`Session`通过`Interpreter`创建,是推理数据的持有者。多个推理可以共用同一个模型,即,多个`Session`可以共用一个`Interpreter`
在创建完`Session`,且不再创建`Session`或更新训练模型数据时,`Interpreter`可以通过`releaseModel`函数释放模型数据,以节省内存。
### 创建Interpreter
有两种创建Interpreter的方法:
- 通过磁盘文件创建
```cpp
/**
* @brief create net from file.
* @param file given file.
* @return created net if success, NULL otherwise.
*/
static Interpreter* createFromFile(const char* file);
```
- 通过内存数据创建
```cpp
/**
* @brief create net from buffer.
* @param buffer given data buffer.
* @param size size of data buffer.
* @return created net if success, NULL otherwise.
*/
static Interpreter* createFromBuffer(const void* buffer, size_t size);
```
**函数返回的Interpreter实例是通过`new`创建的,务必在不再需要时,通过`delete`释放,以免造成内存泄露。**
### 创建Session
一般通过`Interpreter::createSession`创建Session
```cpp
/**
* @brief create session with schedule config. created session will be managed in net.
* @param config session schedule config.
* @return created session if success, NULL otherwise.
*/
Session* createSession(const ScheduleConfig& config);
```
函数返回的Session实例是由Interpreter管理,随着Interpreter销毁而释放,一般不需要关注。也可以在不再需要时,调用`Interpreter::releaseSession`释放,减少内存占用。
**创建Session 一般而言需要较长耗时,而Session在多次推理过程中可以重复使用,建议只创建一次多次使用。**
#### 简易模式
一般情况下,不需要额外设置调度配置,函数会根据模型结构自动识别出调度路径、输入输出,例如:
```cpp
ScheduleConfig conf;
Session* session = interpreter->createSession(conf);
```
#### 调度配置
调度配置定义如下:
```cpp
/** session schedule config */
struct ScheduleConfig {
/** which tensor should be kept */
std::vector<std::string> saveTensors;
/** forward type */
MNNForwardType type = MNN_FORWARD_CPU;
/** CPU:number of threads in parallel , Or GPU: mode setting*/
union {
int numThread = 4;
int mode;
};
/** subpath to run */
struct Path {
std::vector<std::string> inputs;
std::vector<std::string> outputs;
enum Mode {
/**
* Op Mode
* - inputs means the source op, can NOT be empty.
* - outputs means the sink op, can be empty.
* The path will start from source op, then flow when encounter the sink op.
* The sink op will not be compute in this path.
*/
Op = 0,
/**
* Tensor Mode
* - inputs means the inputs tensors, can NOT be empty.
* - outputs means the outputs tensors, can NOT be empty.
* It will find the pipeline that compute outputs from inputs.
*/
Tensor = 1
};
/** running mode */
Mode mode = Op;
};
Path path;
/** backup backend used to create execution when desinated backend do NOT support any op */
MNNForwardType backupType = MNN_FORWARD_CPU;
/** extra backend config */
BackendConfig* backendConfig = nullptr;
};
```
推理时,主选后端由`type`指定,默认为CPU。若模型中存在主选后端不支持的算子,这些算子会使用由`backupType`指定的备选后端运行。
推理路径包括由`path``inputs``outputs`途径的所有算子,在不指定时,会根据模型结构自动识别。为了节约内存,MNN会复用`outputs`之外的tensor内存。如果需要保留中间tensor的结果,可以使用`saveTensors`保留tensor结果,避免内存复用。
CPU推理时,并发数与线程数可以由`numThread`修改。`numThread`决定并发数的多少,但具体线程数和并发效率,不完全取决于`numThread`
- iOS,线程数由系统GCD决定;
- 启用`MNN_USE_THREAD_POOL`时,线程数取决于第一次配置的大于1的`numThread`
- OpenMP,线程数全局设置,实际线程数取决于最后一次配置的`numThread`
GPU推理时,可以通过mode来设置GPU运行的一些参量选择(暂时只支持OpenCL)。GPU mode参数如下:
```c
typedef enum {
// choose one tuning mode Only
MNN_GPU_TUNING_NONE = 1 << 0,/* Forbidden tuning, performance not good */
MNN_GPU_TUNING_HEAVY = 1 << 1,/* heavily tuning, usually not suggested */
MNN_GPU_TUNING_WIDE = 1 << 2,/* widely tuning, performance good. Default */
MNN_GPU_TUNING_NORMAL = 1 << 3,/* normal tuning, performance may be ok */
MNN_GPU_TUNING_FAST = 1 << 4,/* fast tuning, performance may not good */
// choose one opencl memory mode Only
/* User can try OpenCL_MEMORY_BUFFER and OpenCL_MEMORY_IMAGE both, then choose the better one according to performance*/
MNN_GPU_MEMORY_BUFFER = 1 << 6,/* User assign mode */
MNN_GPU_MEMORY_IMAGE = 1 << 7,/* User assign mode */
} MNNGpuMode;
```
目前支持tuning力度以及GPU memory用户可自由设置。例如:
```c
MNN::ScheduleConfig config;
config.mode = MNN_GPU_TUNING_NORMAL | MNN_GPU_MEMORY_IMAGE;
```
tuning力度选取越高,第一次初始化耗时越多,推理性能越佳。如果介意初始化时间过长,可以选取MNN_GPU_TUNING_FAST或者MNN_GPU_TUNING_NONE,也可以同时通过下面的cache机制,第二次之后就不会慢。GPU_Memory用户可以指定使用MNN_GPU_MEMORY_BUFFER或者MNN_GPU_MEMORY_IMAGE,用户可以选择性能更佳的那一种。如果不设定,框架会采取默认判断帮你选取(不保证一定性能最优)。
**上述CPU的numThread和GPU的mode,采用union联合体方式,共用同一片内存。用户在设置的时候numThread和mode只需要设置一种即可,不要重复设置。**
**对于GPU初始化较慢的问题,提供了Cache机制**。后续可以直接加载cache提升初始化速度。
- 具体可以参考tools/cpp/MNNV2Basic.cpp里面setCacheFile设置cache方法进行使用。
- 当模型推理输入尺寸有有限的多种时,每次resizeSession后调用updateCacheFile更新cache文件。
- 当模型推理输入尺寸无限随机变化时,建议config.mode设为1,关闭MNN_GPU_TUNING。
此外,可以通过`backendConfig`设定后端的额外参数。具体见下。
#### 后端配置
后端配置定义如下:
```c
struct BackendConfig {
enum MemoryMode {
Memory_Normal = 0,
Memory_High,
Memory_Low
};
MemoryMode memory = Memory_Normal;
enum PowerMode {
Power_Normal = 0,
Power_High,
Power_Low
};
PowerMode power = Power_Normal;
enum PrecisionMode {
Precision_Normal = 0,
Precision_High,
Precision_Low,
Precision_Low_BF16
};
PrecisionMode precision = Precision_Normal;
/** user defined context */
union {
void* sharedContext = nullptr;
size_t flags; // Valid for CPU Backend
};
};
```
`memory``power``precision`分别为内存、功耗和精度偏好。支持这些选项的后端会在执行时做出相应调整;若不支持,则忽略选项。
示例:
后端 **OpenCL**
**precision 为 Low 时,使用 fp16 存储与计算**,计算结果与CPU计算结果有少量误差,实时性最好;precision 为 Normal 时,使用 fp16存储,计算时将fp16转为fp32计算,计算结果与CPU计算结果相近,实时性也较好;precision 为 High 时,使用 fp32 存储与计算,实时性下降,但与CPU计算结果保持一致。
后端 CPU
**precision 为 Low 时,根据设备情况开启 FP16 计算**
**precision 为 Low_BF16 时,根据设备情况开启 BF16 计算**
`sharedContext`用于自定义后端,用户可以根据自身需要赋值。
#### CPU 核心绑定
MNN 支持将计算任务绑定到指定的 CPU 核心上执行。这对于需要精细控制 CPU 资源,避免线程在不同核心之间切换,或者希望将 MNN 的计算任务限制在特定核心上,以减少对其他应用干扰的场景非常有用。
通过 `Interpreter::setSessionHint` 方法,并使用 `HintMode::CPU_CORE_IDS`,可以指定一个或多个 CPU 核心的 ID。
```cpp
#include <MNN/Interpreter.hpp>
// ...
// 创建 Interpreter
auto interpreter = std::shared_ptr<MNN::Interpreter>(MNN::Interpreter::createFromFile("your_model.mnn"));
// 设置 CPU 核心绑定
// 假设我们希望将计算任务绑定到 0 号和 1 号 CPU 核心
std::vector<int> cpu_ids = {0, 1};
interpreter->setSessionHint(MNN::Interpreter::HintMode::CPU_CORE_IDS, cpu_ids.data(), cpu_ids.size());
// 创建 Session
MNN::ScheduleConfig config;
config.type = MNN_FORWARD_CPU;
config.numThread = 2; // 线程数最好和绑定的核心数一致
auto session = interpreter->createSession(config);
// ... 运行推理
```
**注意事项:**
* `CPU_CORE_IDS` 的设置必须在 `createSession` 之前完成。
* `numThread` 的数量最好设置为与绑定的 CPU 核心数量一致,以达到最佳的性能。
* 如果指定的 CPU 核心 ID 不存在或无效,MNN 将会忽略该配置,并使用默认的线程调度策略。
### 创建多段路径Session
需要对推理路径做出更为复杂的配置时,可以通过调度配置组来实现:
```cpp
/**
* @brief create multi-path session with schedule configs. created session will be managed in net.
* @param configs session schedule configs.
* @return created session if success, NULL otherwise.
*/
Session* createMultiPathSession(const std::vector<ScheduleConfig>& configs);
```
每个调度配置可以独立配置路径、选项。
### 共享运行时资源
默认情况下,在createSession时对应create单独一个 Runtime。对于串行的一系列模型,可以先单独创建Runtime ,然后在各 Session 创建时传入,使各模型用共享同样的运行时资源(对CPU而言为线程池、内存池,对GPU而言Kernel池等)。
示例:
```cpp
ScheduleConfig config;
config.numberThread = 4;
auto runtimeInfo = Interpreter::createRuntime({config});
/*创建第一个模型*/
std::shared_ptr<Interpreter> net1 = Interpreter::createFromFile("1.mnn");
auto session1 = net1->createSession(config, runtimeInfo);
/*创建第二个模型*/
std::shared_ptr<Interpreter> net2 = Interpreter::createFromFile("2.mnn");
auto session2 = net2->createSession(config, runtimeInfo);
/*创建第三个模型*/
std::shared_ptr<Interpreter> net3 = Interpreter::createFromFile("3.mnn");
auto session3 = net3->createSession(config, runtimeInfo);
// 这样 session1, session2, session3 共用同一个Runtime
/*使用*/
/* 填充输入1..... */
net1->runSession(session1);
/* 读取输出1 填充输入2..... */
net2->runSession(session2);
/* 读取输出2 填充输入3..... */
net3->runSession(session3);
```
*** 使用共享运行时的情况下,输入数据需要采用拷贝填充/映射填充,不能使用直接填充:如果后续的Session所需的中间内存比前面的Session大,则会重新申请中间内存,此时前面的Session所使用的中间内存会被释放,若采用拷贝填充/映射填充,MNN中保证了下一次使用Session时中间内存会重新适配,若是采用直接填充,则可能由于使用非法指针而出现内存异常。 *
## 输入数据
### 获取输入tensor
```cpp
/**
* @brief get input tensor for given name.
* @param session given session.
* @param name given name. if NULL, return first input.
* @return tensor if found, NULL otherwise.
*/
Tensor* getSessionInput(const Session* session, const char* name);
/**
* @brief get all input tensors.
* @param session given session.
* @return all output tensors mapped with name.
*/
const std::map<std::string, Tensor*>& getSessionInputAll(const Session* session) const;
```
`Interpreter`上提供了两个用于获取输入`Tensor`的方法:`getSessionInput`用于获取单个输入tensor
`getSessionInputAll`用于获取输入tensor映射。
在只有一个输入tensor时,可以在调用`getSessionInput`时传入NULL以获取tensor。
### 【推荐】映射填充数据
**映射输入Tensor的内存,部分后端可以免数据拷贝**
```cpp
auto input = interpreter->getSessionInput(session, NULL);
void* host = input->map(MNN::Tensor::MAP_TENSOR_WRITE, input->getDimensionType());
// fill host memory data
input->unmap(MNN::Tensor::MAP_TENSOR_WRITE, input->getDimensionType(), host);
```
### 【不推荐】拷贝填充数据
NCHW示例,适用 ONNX / Caffe / Torchscripts 转换而来的模型:
```cpp
auto inputTensor = interpreter->getSessionInput(session, NULL);
auto nchwTensor = new Tensor(inputTensor, Tensor::CAFFE);
// nchwTensor-host<float>()[x] = ...
inputTensor->copyFromHostTensor(nchwTensor);
delete nchwTensor;
```
NHWC示例,适用于由 Tensorflow / Tflite 转换而来的模型:
```cpp
auto inputTensor = interpreter->getSessionInput(session, NULL);
auto nhwcTensor = new Tensor(inputTensor, Tensor::TENSORFLOW);
// nhwcTensor-host<float>()[x] = ...
inputTensor->copyFromHostTensor(nhwcTensor);
delete nhwcTensor;
```
通过这类拷贝数据的方式,用户只需要关注自己创建的tensor的数据布局,`copyFromHostTensor`会负责处理数据布局上的转换(如需)和后端间的数据拷贝(如需)。
### 【已废弃】直接填充数据
```cpp
auto inputTensor = interpreter->getSessionInput(session, NULL);
inputTensor->host<float>()[0] = 1.f;
```
`Tensor`上最简洁的输入方式是直接利用`host`填充数据,但这种使用方式仅限于CPU后端,其他后端需要通过`deviceid`来输入。另一方面,用户需要自行处理`NC4HW4``NHWC`数据格式上的差异。
对于非CPU后端,或不熟悉数据布局的用户,宜使用拷贝数据接口。
### 图像处理
MNN中提供了CV模块,可以帮助用户简化图像的处理,还可以免于引入opencv、libyuv等图片处理库。
1. 支持目标Tensor为float或 uint8_t 的数据格式
2. 支持目标Tensor为NC4HW4或NHWC的维度格式
3. CV模块支持直接输入Device Tensor,也即由Session中获取的Tensor。
#### 图像处理配置
```cpp
struct Config
{
Filter filterType = NEAREST;
ImageFormat sourceFormat = RGBA;
ImageFormat destFormat = RGBA;
//Only valid if the dest type is float
float mean[4] = {0.0f,0.0f,0.0f, 0.0f};
float normal[4] = {1.0f, 1.0f, 1.0f, 1.0f};
};
```
`CV::ImageProcess::Config`
- 通过`sourceFormat``destFormat`指定输入和输出的格式,当前支持`RGBA``RGB``BGR``GRAY``BGRA``YUV_NV21、YUV_NV12`
- 通过`filterType`指定插值的类型,当前支持`NEAREST``BILINEAR``BICUBIC`三种插值方式
- 通过`mean``normal`指定均值归一化,但数据类型不是浮点类型时,设置会被忽略,内部计算过程参考如下伪码
```cpp
for (int i=0; i<bpp; ++i) {
y[i] = (x[i] - mean[i]) * normal[i];
}
```
- `mean`, `normal` 与 torchvision.transforms.Normalize(mean, std) 的对应关系是:
```
mnn.cv.mean = torch.mean * 255.0
mnn.cv.normal = 1.0 / torch.std /255.0
```
#### 图像变换矩阵
`CV::Matrix`移植自Android 系统使用的Skia引擎,用法可参考Skia的Matrix[https://skia.org/user/api/SkMatrix_Reference](https://skia.org/user/api/SkMatrix_Reference)。
需要注意的是,ImageProcess中设置的Matrix是从目标图像到源图像的变换矩阵。使用时,可以按源图像到目标图像的变换设定,最后取逆。例如:
```cpp
// 源图像:1280x720
// 目标图像:逆时针旋转90度再缩小到原来的1/10,即变为72x128
Matrix matrix;
// 重设为单位矩阵
matrix.setIdentity();
// 缩小,变换到 [0,1] 区间:
matrix.postScale(1.0f / 1280, 1.0f / 720);
// 以中心点[0.5, 0.5]旋转90度
matrix.postRotate(90, 0.5f, 0.5f);
// 放大回 72x128
matrix.postScale(72.0f, 128.0f);
// 转变为 目标图像 -> 源图的变换矩阵
matrix.invert(&matrix);
```
#### 图像处理实例
MNN中使用`CV::ImageProcess`进行图像处理。`ImageProcess`内部包含一系列缓存,为了避免内存的重复申请释放,建议将其作缓存,仅创建一次。我们使用`ImageProcess``convert`填充tensor数据。
```cpp
/*
* source: 源图像地址
* iw: 源图像宽
* ih:源图像高,
* stride:源图像对齐后的一行byte数(若不需要对齐,设成 0(相当于 iw*bpp))
* dest: 目标 tensor,可以为 uint8 或 float 类型
*/
ErrorCode convert(const uint8_t* source, int iw, int ih, int stride, Tensor* dest);
```
#### 完整示例
```cpp
auto input = net->getSessionInput(session, NULL);
auto output = net->getSessionOutput(session, NULL);
auto dims = input->shape();
int bpp = dims[1];
int size_h = dims[2];
int size_w = dims[3];
auto inputPatch = argv[2];
FREE_IMAGE_FORMAT f = FreeImage_GetFileType(inputPatch);
FIBITMAP* bitmap = FreeImage_Load(f, inputPatch);
auto newBitmap = FreeImage_ConvertTo32Bits(bitmap);
auto width = FreeImage_GetWidth(newBitmap);
auto height = FreeImage_GetHeight(newBitmap);
FreeImage_Unload(bitmap);
Matrix trans;
//Dst -> [0, 1]
trans.postScale(1.0/size_w, 1.0/size_h);
//Flip Y (因为 FreeImage 解出来的图像排列是Y方向相反的)
trans.postScale(1.0,-1.0, 0.0, 0.5);
//[0, 1] -> Src
trans.postScale(width, height);
ImageProcess::Config config;
config.filterType = NEAREST;
float mean[3] = {103.94f, 116.78f, 123.68f};
float normals[3] = {0.017f,0.017f,0.017f};
::memcpy(config.mean, mean, sizeof(mean));
::memcpy(config.normal, normals, sizeof(normals));
config.sourceFormat = RGBA;
config.destFormat = BGR;
std::shared_ptr<ImageProcess> pretreat(ImageProcess::create(config));
pretreat->setMatrix(trans);
pretreat->convert((uint8_t*)FreeImage_GetScanLine(newBitmap, 0), width, height, 0, input);
net->runSession(session);
```
### 可变维度
```cpp
/**
* @brief resize given tensor.
* @param tensor given tensor.
* @param dims new dims. at most 6 dims.
*/
void resizeTensor(Tensor* tensor, const std::vector<int>& dims);
/**
* @brief resize given tensor by nchw.
* @param batch / N.
* @param channel / C.
* @param height / H.
* @param width / W
*/
void resizeTensor(Tensor* tensor, int batch, int channel, int height, int width);
/**
* @brief call this function to get tensors ready. output tensor buffer (host or deviceId) should be retrieved
* after resize of any input tensor.
* @param session given session.
*/
void resizeSession(Session* session);
```
在输入Tensor维度不确定或需要修改时,需要调用`resizeTensor`来更新维度信息。这种情况一般发生在未设置输入维度和输入维度信息可变的情况。更新完所有Tensor的维度信息之后,需要再调用`resizeSession`来进行预推理,进行内存分配及复用。示例如下:
```cpp
auto inputTensor = interpreter->getSessionInput(session, NULL);
interpreter->resizeTensor(inputTensor, {newBatch, newChannel, newHeight, newWidth});
interpreter->resizeSession(session);
inputTensor->copyFromHostTensor(imageTensor);
interpreter->runSession(session);
```
## 运行会话
MNN中,`Interpreter`一共提供了三个接口用于运行`Session`,但一般来说,简易运行就足够满足绝对部分场景。
### 简易运行
```cpp
/**
* @brief run session.
* @param session given session.
* @return result of running.
*/
ErrorCode runSession(Session* session) const;
```
传入事先创建好的`Session`即可。
函数耗时并不总是等于推理耗时 —— 在CPU下,函数耗时即推理耗时;在其他后端下,函数可能不会同步等待推理完成,例如GPU下,函数耗时为GPU指令提交耗时。
### 回调运行
```cpp
typedef std::function<bool(const std::vector<Tensor*>&,
const std::string& /*opName*/)> TensorCallBack;
/*
* @brief run session.
* @param session given session.
* @param before callback before each op. return true to run the op; return false to skip the op.
* @param after callback after each op. return true to continue running; return false to interrupt the session.
* @param sync synchronously wait for finish of execution or not.
* @return result of running.
*/
ErrorCode runSessionWithCallBack(const Session* session,
const TensorCallBack& before,
const TensorCallBack& end,
bool sync = false) const;
```
相比于简易运行,回调运行额外提供了:
- 每一个op执行前的回调,可以用于跳过Op执行;
- 每一个op执行后的回调,可以用于中断整个推理;
- 同步等待选项,默认关闭;开启时,所有后端均会等待推理完成,即函数耗时等于推理耗时;
### 计算量评估
```cpp
class MNN_PUBLIC OperatorInfo {
struct Info;
public:
/** Operator's name*/
const std::string& name() const;
/** Operator's type*/
const std::string& type() const;
/** Operator's flops, in M*/
float flops() const;
protected:
OperatorInfo();
~OperatorInfo();
Info* mContent;
};
typedef std::function<bool(const std::vector<Tensor*>&, const OperatorInfo*)> TensorCallBackWithInfo;
/*
* @brief run session.
* @param session given session.
* @param before callback before each op. return true to run the op; return false to skip the op.
* @param after callback after each op. return true to continue running; return false to interrupt the session.
* @param sync synchronously wait for finish of execution or not.
* @return result of running.
*/
ErrorCode runSessionWithCallBackInfo(const Session* session,
const TensorCallBackWithInfo& before,
const TensorCallBackWithInfo& end,
bool sync = false) const;
```
一般而言,只有在评估计算量时才会用到的接口。相比于回调运行,在回调时,增加了Op类型和计算量信息。
## 获取输出
### 获取输出tensor
```cpp
/**
* @brief get output tensor for given name.
* @param session given session.
* @param name given name. if NULL, return first output.
* @return tensor if found, NULL otherwise.
*/
Tensor* getSessionOutput(const Session* session, const char* name);
/**
* @brief get all output tensors.
* @param session given session.
* @return all output tensors mapped with name.
*/
const std::map<std::string, Tensor*>& getSessionOutputAll(const Session* session) const;
```
`Interpreter`上提供了两个用于获取输出`Tensor`的方法:`getSessionOutput`用于获取单个输出tensor
`getSessionOutputAll`用于获取输出tensor映射。
在只有一个输出tensor时,可以在调用`getSessionOutput`时传入NULL以获取tensor。
**注意:当`Session`析构之后使用`getSessionOutput`获取的`Tensor`将不可用**
### 【推荐】映射输出数据
**映射输出Tensor的内存数据,部分后端可以免数据拷贝**
```cpp
auto outputTensor = net->getSessionOutput(session, NULL);
void* host = outputTensor->map(MNN::Tensor::MAP_TENSOR_READ, outputTensor->getDimensionType());
// use host memory by yourself
outputTensor->unmap(MNN::Tensor::MAP_TENSOR_READ, outputTensor->getDimensionType(), host);
```
### 【不推荐】拷贝输出数据
**采用纯内存拷贝的方式,拷贝需要花费时间**
NCHW (适用于 Caffe / TorchScript / Onnx 转换而来的模型)示例:
```cpp
auto outputTensor = interpreter->getSessionOutput(session, NULL);
auto nchwTensor = new Tensor(outputTensor, Tensor::CAFFE);
outputTensor->copyToHostTensor(nchwTensor);
auto score = nchwTensor->host<float>()[0];
auto index = nchwTensor->host<float>()[1];
// ...
delete nchwTensor;
```
NHWC (适用于 Tensorflow / Tflite 转换而来的模型)示例:
```cpp
auto outputTensor = interpreter->getSessionOutput(session, NULL);
auto nhwcTensor = new Tensor(outputTensor, Tensor::TENSORFLOW);
outputTensor->copyToHostTensor(nhwcTensor);
auto score = nhwcTensor->host<float>()[0];
auto index = nhwcTensor->host<float>()[1];
// ...
delete nhwcTensor;
```
**通过这类拷贝数据的方式,用户只需要关注自己创建的tensor的数据布局,`copyToHostTensor`会负责处理数据布局上的转换(如需)和后端间的数据拷贝(如需)。**
### 【已废弃】直接读取数据
**由于绝大多数用户都不熟悉MNN底层数据布局,所以不要使用这种方式!!!**
```cpp
auto outputTensor = interpreter->getSessionOutput(session, NULL);
auto score = outputTensor->host<float>()[0];
auto index = outputTensor->host<float>()[1];
// ...
```
`Tensor`上最简洁的输出方式是直接读取`host`数据,但这种使用方式仅限于CPU后端,其他后端需要通过`deviceid`来读取数据。另一方面,用户需要自行处理`NC4HW4``NHWC`数据格式上的差异。
**对于非CPU后端,或不熟悉数据布局的用户,宜使用拷贝数据接口。**
## 示例代码
完整的示例代码可以参考`demo/exec/`文件夹中的以下源码文件:
- `pictureRecognition.cpp` 使用`Session`执行模型推理进行图片分类,使用`ImageProcess`进行前处理
- `multiPose.cpp` 使用`Session`执行模型推理进行姿态检测,使用`ImageProcess`进行前处理
- `segment.cpp` 使用`Session`执行模型推理进行图像分割,使用`ImageProcess`进行前处理,`Expr`进行后处理
- `pictureRotate.cpp` 使用`ImageProcess`进行图像处理