还不会使用MIGraphX推理?试试这篇让你快速入门

举报
染念 发表于 2023/11/30 20:58:16 2023/11/30
【摘要】 使用MIGraphX进行推理一般包括下面几个步骤:创建模型低精度优化编译执行推理,并返回结果 创建模型MIGraphX 支持两种方式创建模型:加载 ONNX 模型和使用 API 手动创建。 ONNX 模型首先要将常用的权重模型文件转换onnx格式,下面展示了常见的pytorch框架转向onnx。 转换import torchimport torchvision# 模型文件# https://...

使用MIGraphX进行推理一般包括下面几个步骤:

  1. 创建模型
  2. 低精度优化
  3. 编译
  4. 执行推理,并返回结果

创建模型

MIGraphX 支持两种方式创建模型:加载 ONNX 模型和使用 API 手动创建。

ONNX 模型

首先要将常用的权重模型文件转换onnx格式,下面展示了常见的pytorch框架转向onnx。

转换

import torch
import torchvision

# 模型文件
# https://download.pytorch.org/models/resnet50-19c8e357.pth
pathOfModel = "resnet50-19c8e357.pth"
# 创建 PyTorch ResNet50 模型实例
net = torchvision.models.resnet50(pretrained=False)

# 定义一个 PyTorch 张量来模拟输入数据
input_data = torch.randn(32,3,224,224)

# 将模型转换为 ONNX 格式
output_path = "resnet50.onnx"
net.load_state_dict(torch.load(pathOfModel))
net.eval()
torch.onnx.export(net, input_data, output_path,
                  input_names=["input"])

加载模型

加载头文件:#include <migraphx/onnx.hpp>

// 加载模型
migraphx::program net= migraphx::parse_onnx("resnet50.onnx");

这种办法十分方便。加载好模型之后,可以通过program的get_parameter_shapes()函数获取网络的输入属性。

低精度优化

加载头文件#include <migraphx/quantization.hpp>
如果需要采用FP16模式进行推理,可以通过migraphx::quantize_fp16(net);函数实现,如果没有设置,则默认采用FP32模式。MIGraphX同时也支持int8推理,我们会在后面讲到如何使用int8模式。

编译

加载头文件:#include <migraphx/gpu/target.hpp>
加载onnx模型之后,需要使用net.compile(migraphx::gpu::target{},options)方法编译模型。这里将模型编译为GPU模式,如果需要编译为CPU模式,需要使用migraphx::cpu::target{}

推理

  1. 编译好模型之后,需要输入数据,输入数据需要经过预处理并转换为NCHW的格式。
  2. 通过net的eval()方法执行推理计算,eval()方法执行完成之后,会返回推理结果,推理结果是一个std::vector<argument>类型,推理的结果是host端数据,然后我们就可以通过argument提供的成员函数去访问推理结果了。

完整代码

增加了softmax的计算,参考了附录2的代码

#include <string>
#include <vector>
#include <migraphx/onnx.hpp>
#include <migraphx/gpu/target.hpp>
#include <migraphx/quantization.hpp>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;
using namespace cv::dnn;
using namespace migraphx;

typedef struct  _ResultOfPrediction
{
    float confidence;
    int label;
    _ResultOfPrediction():confidence(0.0f),label(0){}

}ResultOfPrediction;

std::vector<float> ComputeSoftmax(const std::vector<float>& results)
{
    // 计算最大值
    float maxValue=-3.40e+38F; // min negative value
    for(int i=0;i<results.size();++i)
    {
        if(results[i]>maxValue)
        {
            maxValue=results[i];
        }

    }

    // 计算每一类的softmax概率
    std::vector<float> softmaxResults(results.size());
    float sum=0.0;
    for(int i=0;i<results.size();++i)
    {
        softmaxResults[i]= exp((float)(results[i] - maxValue));
        sum+=softmaxResults[i];
    }
    for(int i=0;i<results.size();++i)
    {
       softmaxResults[i]= softmaxResults[i]/sum;
    }
    
    return softmaxResults;

}

int main(int argc, char *argv[])
{
    // 加载模型
    migraphx::program net= migraphx::parse_onnx("resnet50.onnx");

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    string inputName=inputAttribute.first;
    migraphx::shape inputShape=inputAttribute.second; 
    int N=inputShape.lens()[0];
    int C=inputShape.lens()[1];
    int H=inputShape.lens()[2];
    int W=inputShape.lens()[3];
    printf("input name:%s\n",inputName.c_str());
    printf("input shape:%d,%d,%d,%d\n",N,C,H,W);

    // 使用FP16
    migraphx::quantize_fp16(net);

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;//默认为0号设备
    // 注意:如果你的输入数据在host端,则在设置编译选项的时候,需要设置offload_copy为true。
    options.offload_copy=true;
    net.compile(migraphx::gpu::target{},options);// GPU模式
    
    // 预处理并转换为NCHW
    int batchSize=N;
    Mat srcImage=imread("Test.jpg");
    vector<Mat> srcImages;
    for(int i=0;i<batchSize;++i)
    {
        srcImages.push_back(srcImage);
    }
    Mat inputBlob;
    blobFromImages(srcImages,inputBlob,0.0078125,cv::Size(W,H),cv::Scalar(127.5,127.5,127.5),false,false);

    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    migraphx::argument result  = results[0]; // 获取第一个输出节点的数据
    migraphx::shape outputShape=result.get_shape(); // 输出节点的shape
    std::vector<std::size_t> outputSize=outputShape.lens();// 每一维大小,维度顺序为(N,C,H,W)
    int numberOfOutput=outputShape.elements();// 输出节点元素的个数
    float *resultData=(float *)result.data();// 输出节点数据指针

    // 获取推理结果
    int numberOfPerImage=numberOfOutput/N; // 每张图像的输出个数
    printf("output size:%d\n",numberOfPerImage);
    for(int i=0;i<N;++i)
    {
        printf("==========%d image output=============\n",i);
        int startIndex=numberOfPerImage*i;
		// 获取每幅图像对应的输出
        std::vector<float> logit;
        for(int j=0;j<numberOfPerImage;++j)
        {
            printf("%f,",resultData[startIndex+j]);
            logit.push_back(resultData[startIndex+j]);
        }
        printf("\n");
        // 计算softmax
        std::vector<float> probs;
        probs = ComputeSoftmax(logit);
        std::vector<ResultOfPrediction> resultOfPredictions;
        for(int j=0;j<numberOfPerImage;++j)
        {
            ResultOfPrediction prediction;
            prediction.label=j;
            prediction.confidence=probs[j];
            resultOfPredictions.push_back(prediction);
        }
        // 一个batch中第i幅图像的结果
        printf("========== %d result ==========\n",i);
        for(int j=0;j<resultOfPredictions.size();++j)
        {
            ResultOfPrediction prediction=resultOfPredictions[j];
            printf("label:%d,confidence:%f\n",prediction.label,prediction.confidence);
        }
        
        
    }

    return 0;

}

模型转换注意点

调整模型输入Shape

在加载onnx模型时,如果需要修改模型的输入shape,可以通过onnx_options参数来实现。

// 设置模型输入shape
migraphx::onnx_options onnx_options;
onnx_options.map_input_dims["input"]={32,3,224,224}
// 加载模型
migraphx::program net= migraphx::parse_onnx("resnet50.onnx",onnx_options);

input表示输入节点名,可以在导出onnx模型时候自定义。

upsample算子不等价

如果遇到ONNX的Upsample算子和PyTorch版本不一致的问题:

  1. 更新PyTorch到最新版本。

  2. 在导出ONNX模型时,设置opset_version参数为11或更高版本。示例代码如下:torch.onnx.export(model, input, filename, verbose=False,opset_version=11,...) # or other number greater than 11

batchnorm参数不固定

在将PyTorch模型转换为ONNX模型时,如果PyTorch未切换到推理模式,可能导致BatchNorm参数不固定。解决方案是,在导出ONNX模型前,将PyTorch切换到推理模式。示例代码如下:torch_model.eval() or
torch_model.train(False)

C++ API

创建模型

创建一个模型:migraphx::program net;十分简单,但却是一个空网络,因此我们要往里面添加模块。

添加Module

模块的添加要根据MIGraphX的定义,它是一个主计算图,通过migraphx::module *mainModule = net.get_main_module();获得,然后往主计算图添加子图。

子图可以是输入,可以是权重,可以是算子,几乎要用的,都要通过子图添加。但是这些使用的函数是不一样的,如果不清楚,请看前面发的内容~
注意:创建算子的时候,如果创建的算子没有属性,则可以直接通过 migraphx::make_op()方法创建。 否则需要通过migraphx::make_json_op("convolution","{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"),input,convKernel);make_json_op函数创建算子。
最后通过add_return()添加结束指令mainModule->add_return({fflatten});,到这里整个模型就创建完成了。然后通过net就可以调用使用了,注意不是module了,而是net!

完整代码

migraphx::program CreateNet()
{
    //创建一个模型
    migraphx::program net;
    //获取主计算图
    migraphx::module *mainModule = net.get_main_module();
    // 添加模型的输入
    migraphx::instruction_ref input =mainModule->add_parameter("input",
    migraphx::shape{migraphx::shape::float type, {1, 1,4,6}});
    // 添加卷积权重
    std::vector<float> weightData(1*1*1*1);
    for(int i=0;i<weightData.size();++i) weightData[i]=1.0;
    migraphx::shape weightShape{migraphx::shape::float type,{1,1,1,1}};
    migraphx::literal convweight{weightShape,weightData};
    migraphx::instruction_ref convKernel= mainModule->add_literal(convweight);
    // 添加卷积算子
    migraphx::instruction_ref conv = mainModule->add_instruction(
        migraphx::make_json_op("convolution","{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"),
        input,
        convKernel);
    // 添加slice算子
    migraphx::instruction_ref slice = mainModule->add_instruction(
        migraphx::make_json_op("slice", "{axes:[2,3], starts:[0,2],ends:[4,5]}"),conv);
    // 添加contiguous算子
    migraphx::instruction_ref contiguous = mainModule->add_instruction(migraphx::make_op("contiguous"), slice);
    // 添加flatten算子
    migraphx::instruction_ref flatten = mainModule->add_instruction(migraphx::make_op("flatten"), contiguous);
    // 添加return
    mainModule->add_return({fflatten});
    return net;
}
int main(int argc, char* argv[])
{
    // 创建模型
    migraphx::program net= CreateNet();
    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;// 设置GPU设备,默认为0号设备
    options.offload_copy=true; // 设置offload_copy
    net.compile(migraphx::gpu::target{},options);// GPU模式
    // 输入数据
    std::vector<float> inputData (1*1*4*6);
    for(int i=0;i<inputData.size();++i) inputData[i]=i;
    migraphx::shape inputShape(migraphx::shape::float_type,{1,1,4,6});
    migraphx::argument data{inputShape,inputData.data()};
    migraphx::parameter_map inputDataMap;
    inputDataMap["input"]=data;
    // 推理
    std::vector<migraphx::argument> results = net.eval(inputDataMap);
    // 获取推理结果
    migraphx::argument result = results[0]; // 获取第一个输出节点的数据
    migraphx::shape outputShape=result.get_shape(); // 输出节点的shape
    int numberOfOutput=outputShape.elements();// 输出节点元素的个数
    float *resultData=(float *)result.data();// 输出节点数据指针
    for(int i=0;i<numberofoutput;++i) printf("%d,",resultData[i]);
    printf("\n");
    return 0;
}
//输出:2,3,4,8,9,10,14,155,16,20,21,22
  • 通过migraphx::parameter_map创建模型的输入, parameter_map表示输入的映射关系。
  • data变量如下所示:NCHW,所以存储的是4行6列的数据。
0 1   |2  3   4| 5
6 7   |8  9  10| 11
12 13 |14 15 16| 17
18 19 |20 21 22| 23

Python API

使用python接口首先设置环境变量:export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
下面看一下基本使用方法,之后可以直接在这个代码基础上进行修改。

from PIL import Image
import numpy as np
import migraphx

def ReadImage(pathOfImage,inputShape):
    resizedImage = Image.open(pathOfImage).resize( (inputShape[3], inputShape[2]) )
    srcImage = np.asarray(resizedImage).astype("float32")

    # 转换为NCHW
    srcImage_NCHW = np.transpose(srcImage, (2, 0, 1))

    # 预处理
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inputData = np.zeros(srcImage_NCHW.shape).astype("float32")
    for i in range(srcImage_NCHW.shape[0]):
        inputData[i, :, :] = (srcImage_NCHW[i, :, :]/ 255 - mean[i]) / std[i]

    # 增加batch维度
    imageData = np.expand_dims(inputData, axis=0)

    return imageData

def Softmax(x):
    return np.exp(x)/sum(np.exp(x))

if __name__ == '__main__':
    # 加载模型
    model = migraphx.parse_onnx("resnet50.onnx")
    inputName=model.get_parameter_names()[0]
    inputShape=model.get_parameter_shapes()[inputName].lens()
    print("inputName:{0} \ninputShape:{1}".format(inputName,inputShape))

    # FP16
    migraphx.quantize_fp16(model)

    # 编译
    model.compile(migraphx.get_target("gpu"),device_id=0)

    # 读取图像
    pathOfImage ="Test.jpg"
    image = ReadImage(pathOfImage,inputShape)

    # 推理
    results = model.run({inputName: migraphx.argument(image)})

    # 获取输出节点属性
    result=results[0] # 获取第一个输出节点的数据,migraphx.argument类型
    outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型
    outputSize=outputShape.lens() # 每一维大小,维度顺序为(N,C,H,W),list类型
    numberOfOutput=outputShape.elements() # 输出节点元素的个数

    # 获取输出结果
    resultData=result.tolist() # 输出数据转换为list
    result = np.array(resultData)
    scores = Softmax(result) # 计算softmax
    print(scores)

上面提供了opencv版的读取图片,这里是使用PIL版本:

def ReadImage(pathOfImage, inputShape):
    srcImage = cv2.imread(pathOfImage, cv2.IMREAD_COLOR)# numpy类型, HWC# resize并转换为CHW
	resizedImage = cv2.resize(srcImage, (inputShape[3], inputShape[2]))
	resizedImage_Float = resizedImage.astype("float32")# 转换为float32
	srcImage_CHW = np.transpose(resizedImage_Float, (2, 0, 1))# 转换为CHW# 预处理
	mean = np.array([127.5, 127.5, 127.5])
	scale = np.array([0.0078125, 0.0078125, 0.0078125])
	inputData = np.zeros(inputShape).astype("float32")# NCHW
	for i in range(srcImage_CHW.shape[0]):
	    inputData[0, i, : , : ] = (srcImage_CHW[i, : , : ] - mean[i]) * scale[i]# 复制到batch中的其他图像
	for i in range(inputData.shape[0]):
	    if i != 0:
	    inputData[i, : , : , : ] = inputData[0, : , : , : ]
	return inputData

int8优化

fp16优化migraphx::quantize_fp16(net);一句话即可,而int8需要经过3步,输入量化校准数据、计算量化参数、生成量化参数。

代码如下:

// 读取校准数据,本示例这里采用OpenCV读取
Mat srcImage=imread("CalibrationData.jpg",1);
std::vector<cv::Mat> srcImages;
for(int i=0;i<inputShape.lens()[0];++i)
{
    srcImages.push_back(srcImage);
}
Mat inputBlob;
blobFromImages(srcImages,inputBlob,0.0078125,cv::Size(W,H),cv::Scalar(127.5,127.5,127.5),false,false);
migraphx::parameter_map inputData;
inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

// 创建量化数据,这里只使用了一张图像,实际使用时为了提高量化精度,建议使用多张图像创建多个inputData进行量化
std::vector<migraphx::parameter_map> calibrationData = {inputData};

// INT8量化
migraphx::quantize_int8(net, migraphx::gpu::target{}, calibrationData);

为了保证量化精度,建议使用验证集或者测试集中多个典型的数据作为量化校准数据,如果用户没有提供量化校准数据,MIGraphX会使用默认的量化参数,这样可能会导致严重的精度下降。

使用设备数据推理

在某些情况下,输入数据直接位于设备(如 GPU)内存中,可以直接使用设备数据进行推理。

#include <migraphx/gpu/hip.hpp>
// 创建参数映射函数,将程序 `p` 的每个参数转移到 GPU 上。
migraphx::parameter_map CreateParameterMap(migraphx::program &p) {
  migraphx::parameter_map parameterMap;
  for (std::pair<std::string, migraphx::shape> x : p.get_parameter_shapes()) {
    parameterMap[x.first] =
        migraphx::gpu::to_gpu(migraphx::generate_argument(x.second));
  }
  return parameterMap;
}

int main(int argc, char *argv[]) {
  // 加载模型...
  // 获取模型输入属性...
  // 编译模型...
  options.offload_copy =false;  // 设置offload_copy,这里注意:一定要设置为false!
  net.compile(migraphx::gpu::target{}, options);  // GPU模式
  // 为输出节点分配内存
  migraphx::parameter_map parameterMap = CreateParameterMap(net);
  // 预处理并转换为NCHW...
  // 转换为device数据,这里的inputData中的数据是device数据
  migraphx::argument inputData = migraphx::gpu::to_gpu(migraphx::argument{inputShape, (float *)inputBlob.data});
  // 这里直接使用device数据作为输入数据,inputData.data()返回的是device地址
  parameterMap[inputName] = migraphx::argument{inputShape, inputData.data()};
  // 推理...
  // 获取输出节点的属性
  migraphx::argument result = migraphx::gpu::from_gpu(results[0]);  // 将第一个输出节点的数据拷贝到host端
  //输出...
}
  • 一定要将offload_copy设置为false,这样才可以直接使用device数据。
  • 示例中通过migraphx::gpu::to_gpu创建一个device数据,并输入到模型中。
  • 推理的结果需要通过migraphx::gpu::from_gpu()拷贝到host端。

python代码:

import cv2
import numpy as np
import migraphx
import torch
def ReadImage(pathOfImage, inputShape):
    ...
def CreateParameterMap(model):
    parameterMap = {}
    parameter_shapes = model.get_parameter_shapes()
    for key in parameter_shapes.keys():
        parameterMap[key] =
        migraphx.to_gpu(migraphx.generate_argument(s = parameter_shapes[key]))
    return parameterMap
if __name__ == '__main__': #加载模型
	//...
	image = ReadImage(pathOfImage, inputShape)# 转换为gpu tensor
	input_tensor = torch.from_numpy(image).to(torch.device("cuda"))
	# 使用device数据作为输入数据
	parameterMap[inputName] = migraphx.gpudata_to_argument(shape = model.get_parameter_s hapes()[inputName], address = input_tensor.data_ptr())
	# 推理
	results = model.run(parameterMap)
	# 获取输出节点属性
	result = migraphx.from_gpu(results[0])# 将第一个输出节点的数据拷贝到host端,migraphx.argument类型
	//...

模型序列化

我们看到每次都要编译模型,实际上加载编译好的模型之后不需要再次执行编译操作了,可以直接输入数据执行推理,节省编译时间,加快启动速度,同时使用这种方式还可以一定程度上实现对onnx模型的加密。
注意加载头文件#include <migraphx/load_save.hpp> // 添加save和load头文件

onnx序列化到mxr

// 序列化并保存编译好的模型
migraphx::save(net, "ResNet50.mxr");

mxr反序列化到推理

// 加载编译好的模型
migraphx::file_options options;
options.device_id = 1;  // 设置GPU设备,默认为0号设备
migraphx::program net = migraphx::load("ResNet50.mxr", options);
// 获取模型输入属性...

动态Shape

动态shape是在深度学习模型中处理可变输入尺寸的一种技术。这在处理图像数据时尤为重要,因为不同的图像可能有不同的分辨率。利用动态shape,同一个模型可以处理各种尺寸的输入,无需针对每种尺寸重构或调整模型。

在MIGraphX框架中,实现动态shape主要包括以下步骤:

  1. 设置环境变量:通过设置export MIGRAPHX_DYNAMIC_SHAPE=1,启用动态shape功能。
  2. 定义模型的最大输入尺寸:这个尺寸是模型处理输入数据的上限。如果输入超过这个尺寸,模型会报错。
  3. 使用reshape方法适配输入尺寸:通过程序(program)类的reshape方法调整模型,使其适应不同的输入尺寸。
    伪代码:

以下是动态shape在C++和Python中的具体实现示例:

c++代码:

int main(int argc, char *argv[]) {
  // 设置最大输入shape
  migraphx::onnx_options onnx_options;
  onnx_options.map_input_dims["input"] = {2, 3, 512,
                                          512};  // input表示输入节点名
  // 加载模型。。。
  // 编译。。。
  // 设置动态输入,这里添加了2个不同的输入shape
  std::vector<std::vector<std::size_t>> inputShapes;
  inputShapes.push_back({2, 3, 16, 16});
  inputShapes.push_back({2, 3, 32, 32});
  cv::Mat srcImage = cv::imread("Test.jpg", 1);
  for (int i = 0; i < inputShapes.size(); ++i) {
    // 设置输入shape并执行reshape
    std::unordered_map<std::string, std::vector<std::size_t>> inputShapeMap;
    inputShapeMap[inputName] = inputShapes[i];
    net.reshape(inputShapeMap);
    std::vector<cv::Mat> srcImages;
    for (int j = 0; j < inputShapes[i][0]; ++j) {
      srcImages.push_back(srcImage);
    }
    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125,
                            cv::Size(inputShapes[i][3], inputShapes[i][2]),
                            cv::Scalar(127.5, 127.5, 127.5), false, false);
    // 输入数据
    // 推理
    // 获取输出节点的属性
    // 打印输出
  }
  return 0;
}

python代码:

import cv2
import numpy as np
import migraphx

if __name__ == '__main__': 
    #设置最大输入shape
	maxInput = {
	    "input": [2, 3, 512, 512]
	}
	#加载模型..
	# 编译..
	# 设置动态输入, 这里添加了2个不同的输入shape
	inputShapes = [
	    [2, 3, 16, 16],
	    [2, 3, 32, 32]
	]
	for inputShape in inputShapes:
	    inputShapeMap = {
	        inputName: inputShape
	    }
		# 执行reshape
		model.reshape(inputs = inputShapeMap)
		# 预处理并转换为NCHW
		# 推理
		# 获取输出节点属性

总结:代码中,很多前面的代码如输入数据,推理,打印等等是在for循环里的,因为shape会变,每次的输出也都不一样!

动态shape的限制
全连接层:对于包含全连接层的模型,需要在全连接层之前添加全局池化层以保持C、H、W维度上的一致性。
LSTM模型:在包含LSTM的模型中,batch size必须为1。

如果模型不支持动态Shape,可以:

  • 将图像调整到固定大小。
  • 使用零填充方法,将不同大小的图像填充到统一尺寸。

支持动态Shape的模型

支持N,H,W维度变化的模型 仅支持N维度变化的模型 仅支持H,W维度变化的模型
AlexNet ShuffleNet CRNN-LSTM
VGG16 SqueezeNet
VGG19 EfficientNet-B3
GoogLeNet EfficientNet-B5
InceptionV3 EfficientNet-B7
ResNet50 YOLOV2
DenseNet YOLOV3
MobileNetV1 YOLOV5
MobileNetV2 UNet
MobileNetV3 PaddleOCR
MTCNN
SSD-VGG16
RetinaNet
RetinaFace
DBNET
FCN

附录

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。