引言在实际的深度学习应用中模型的输入尺寸往往不是固定的。例如自然语言处理中的文本长度、目标检测中的图像分辨率、语音识别中的音频时长都可能变化。传统的静态shape推理需要为每种输入尺寸编译一个模型既浪费存储空间又增加了部署复杂度。华为CANN平台提供了强大的动态shape推理能力允许模型在运行时适应不同尺寸的输入同时保持高性能。本文将详细介绍CANN动态shape的使用方法和优化技巧。相关链接CANN组织ops-nn仓库一、动态shape的应用场景1.1 为什么需要动态shape在以下场景中动态shape是必不可少的自然语言处理不同句子的长度差异很大从几个词到几百个词不等。如果使用固定长度要么浪费计算资源padding到最大长度要么限制了模型的适用范围。计算机视觉目标检测和分割任务中输入图像的分辨率可能不同视频处理中帧数也是可变的。语音识别音频片段的时长各不相同使用动态shape可以避免不必要的padding。批处理优化在服务端部署时可以根据请求量动态调整batch size提高硬件利用率。1.2 CANN动态shape的优势相比其他推理框架CANN的动态shape实现具有以下优势零拷贝切换在不同shape之间切换时无需重新加载模型自动优化CANN会为常见的shape组合生成优化的执行路径内存高效动态分配内存避免为最大尺寸预留空间性能稳定通过shape缓存机制常用shape的性能接近静态编译二、CANN动态shape基础使用2.1 导出动态shape模型首先我们需要在模型导出时指定哪些维度是动态的。以BERT模型为例importtorchimporttorch.onnx# 加载BERT模型fromtransformersimportBertModel,BertTokenizer modelBertModel.from_pretrained(bert-base-uncased)model.eval()# 准备示例输入tokenizerBertTokenizer.from_pretrained(bert-base-uncased)textHello, CANN dynamic shape!inputstokenizer(text,return_tensorspt)# 导出为ONNX指定动态维度# batch_size和sequence_length都是动态的dynamic_axes{input_ids:{0:batch_size,1:sequence_length},attention_mask:{0:batch_size,1:sequence_length},last_hidden_state:{0:batch_size,1:sequence_length}}torch.onnx.export(model,(inputs[input_ids],inputs[attention_mask]),bert_dynamic.onnx,input_names[input_ids,attention_mask],output_names[last_hidden_state],dynamic_axesdynamic_axes,opset_version14)print(动态shape模型导出完成)2.2 使用ATC转换动态shape模型接下来使用CANN的ATC工具将ONNX模型转换为支持动态shape的OM格式# 转换为动态shape的OM模型atc --modelbert_dynamic.onnx\--framework5\--outputbert_dynamic\--input_shapeinput_ids:-1,-1;attention_mask:-1,-1\--dynamic_dims1,128;1,256;1,512;4,128;4,256;4,512\--soc_versionAscend910\--logerror# 参数说明# --input_shape: -1表示该维度是动态的# --dynamic_dims: 指定支持的shape组合格式为batch,seq_len# 这里支持batch1或4seq_len128/256/512的组合这个命令告诉CANN模型需要支持6种不同的输入shape组合。CANN会为这些组合生成优化的执行计划。2.3 动态shape推理代码使用CANN ACL接口进行动态shape推理importaclimportnumpyasnpclassCANNDynamicInference:CANN动态shape推理类def__init__(self,model_path,device_id0):# 初始化ACLself.device_iddevice_id retacl.init()assertret0,ACL初始化失败retacl.rt.set_device(self.device_id)assertret0,设置设备失败# 加载模型self.model_id,retacl.mdl.load_from_file(model_path)assertret0,模型加载失败self.model_descacl.mdl.create_desc()retacl.mdl.get_desc(self.model_desc,self.model_id)assertret0,获取模型描述失败print(f模型加载成功支持动态shape推理)definfer(self,input_ids,attention_mask):执行动态shape推理batch_size,seq_lengthinput_ids.shapeprint(f推理shape: batch{batch_size}, seq_len{seq_length})# 创建输入数据集input_datasetacl.mdl.create_dataset()# 添加input_idsinput_ids_bufferself._create_data_buffer(input_ids)acl.mdl.add_dataset_buffer(input_dataset,input_ids_buffer)# 添加attention_maskmask_bufferself._create_data_buffer(attention_mask)acl.mdl.add_dataset_buffer(input_dataset,mask_buffer)# 创建输出数据集output_datasetacl.mdl.create_dataset()# 执行推理 - CANN会自动适配当前的shaperetacl.mdl.execute(self.model_id,input_dataset,output_dataset)assertret0,推理执行失败# 获取输出output_bufferacl.mdl.get_dataset_buffer(output_dataset,0)output_dataself._get_buffer_data(output_buffer,(batch_size,seq_length,768))# 清理资源self._destroy_dataset(input_dataset)self._destroy_dataset(output_dataset)returnoutput_datadef_create_data_buffer(self,data):创建数据bufferdata_npdata.astype(np.int32)data_sizedata_np.nbytes# 在设备上分配内存device_ptr,retacl.rt.malloc(data_size,0)assertret0,设备内存分配失败# 拷贝数据到设备retacl.rt.memcpy(device_ptr,data_size,data_np.ctypes.data,data_size,0)assertret0,数据拷贝失败# 创建bufferbufferacl.create_data_buffer(device_ptr,data_size)returnbufferdef_get_buffer_data(self,buffer,shape):从buffer获取数据data_ptracl.get_data_buffer_addr(buffer)data_sizeacl.get_data_buffer_size(buffer)# 分配主机内存host_datanp.zeros(shape,dtypenp.float32)# 从设备拷贝到主机retacl.rt.memcpy(host_data.ctypes.data,data_size,data_ptr,data_size,1)assertret0,数据拷贝失败returnhost_datadef_destroy_dataset(self,dataset):销毁数据集numacl.mdl.get_dataset_num_buffers(dataset)foriinrange(num):bufferacl.mdl.get_dataset_buffer(dataset,i)data_ptracl.get_data_buffer_addr(buffer)acl.rt.free(data_ptr)acl.destroy_data_buffer(buffer)acl.mdl.destroy_dataset(dataset)# 使用示例inferenceCANNDynamicInference(bert_dynamic.om)# 测试不同的输入shapetest_cases[(1,128),# batch1, seq_len128(4,256),# batch4, seq_len256(1,512),# batch1, seq_len512]forbatch,seq_lenintest_cases:input_idsnp.random.randint(0,30000,(batch,seq_len))attention_masknp.ones((batch,seq_len))outputinference.infer(input_ids,attention_mask)print(f输出shape:{output.shape}\n)三、动态shape性能优化3.1 Shape档位设计在使用动态shape时合理设计shape档位gear是性能优化的关键。档位就是在模型转换时通过--dynamic_dims指定的shape组合。档位设计原则覆盖常用场景分析实际业务数据统计最常出现的输入尺寸优先为这些尺寸设置档位避免过多档位档位越多模型文件越大首次推理的编译时间也越长。建议控制在10个以内考虑内存对齐选择能被硬件向量长度整除的尺寸如64、128、256等预留余量为未来可能的需求预留一些档位例如对于BERT模型可以这样设计档位# 推荐的档位设计--dynamic_dims1,64;1,128;1,256;1,512;4,64;4,128;4,256;8,128# 覆盖场景# - 单样本推理batch1支持64/128/256/512长度# - 小批量batch4支持64/128/256长度# - 批处理batch8支持128长度3.2 Shape缓存机制CANN内部实现了shape缓存机制。当某个shape首次出现时CANN会进行即时编译JIT生成针对该shape的优化代码。后续再遇到相同shape时直接使用缓存的代码性能接近静态编译。importtimedefbenchmark_dynamic_shape(inference,shapes,warmup5,iterations50):测试动态shape性能results{}forbatch,seq_leninshapes:input_idsnp.random.randint(0,30000,(batch,seq_len))attention_masknp.ones((batch,seq_len))# 预热 - 触发shape缓存print(f预热 shape ({batch},{seq_len})...)for_inrange(warmup):_inference.infer(input_ids,attention_mask)# 性能测试starttime.time()for_inrange(iterations):_inference.infer(input_ids,attention_mask)elapsed(time.time()-start)/iterations results[(batch,seq_len)]elapsed*1000# 转换为msprint(fShape ({batch},{seq_len}):{elapsed*1000:.2f}ms)returnresults# 测试不同shape的性能shapes[(1,128),(1,256),(4,128),(4,256)]resultsbenchmark_dynamic_shape(inference,shapes)# 分析结果print(\n性能汇总:)forshape,latencyinresults.items():print(f{shape}:{latency:.2f}ms)从测试结果可以看到预热后的推理性能非常稳定说明shape缓存机制工作良好。3.3 内存优化策略动态shape推理的内存管理比静态shape更复杂。CANN提供了几种内存优化策略动态内存分配CANN会根据实际输入的shape动态分配内存避免为最大尺寸预留空间。内存池复用对于频繁出现的shapeCANN会将内存保留在池中减少分配和释放的开销。分段分配对于超大输入CANN支持分段处理避免一次性分配过多内存。# 配置CANN内存策略importacl# 设置内存复用acl.rt.set_op_wait_timeout(100)# 设置算子等待超时# 启用内存优化acl.rt.set_op_debug_level(0)# 关闭调试减少内存开销四、高级应用场景4.1 自适应批处理在服务端部署时可以根据请求量动态调整batch size提高吞吐量importqueueimportthreadingclassCANNDynamicBatcher:CANN动态批处理器def__init__(self,inference,max_batch8,timeout0.01):self.inferenceinference self.max_batchmax_batch self.timeouttimeout self.request_queuequeue.Queue()self.runningTrue# 启动批处理线程self.workerthreading.Thread(targetself._batch_worker)self.worker.start()defpredict(self,input_ids,attention_mask):提交推理请求result_queuequeue.Queue()self.request_queue.put((input_ids,attention_mask,result_queue))returnresult_queue.get()# 等待结果def_batch_worker(self):批处理工作线程whileself.running:batch_requests[]# 收集请求直到达到max_batch或超时try:# 等待第一个请求requestself.request_queue.get(timeoutself.timeout)batch_requests.append(request)# 尝试收集更多请求whilelen(batch_requests)self.max_batch:try:requestself.request_queue.get(timeout0.001)batch_requests.append(request)exceptqueue.Empty:breakexceptqueue.Empty:continue# 执行批量推理ifbatch_requests:self._process_batch(batch_requests)def_process_batch(self,requests):处理一批请求# 合并输入input_ids_list[req[0]forreqinrequests]mask_list[req[1]forreqinrequests]# Padding到相同长度max_lenmax(ids.shape[0]foridsininput_ids_list)batch_sizelen(requests)input_ids_batchnp.zeros((batch_size,max_len),dtypenp.int32)mask_batchnp.zeros((batch_size,max_len),dtypenp.int32)fori,(ids,mask)inenumerate(zip(input_ids_list,mask_list)):lengthids.shape[0]input_ids_batch[i,:length]ids mask_batch[i,:length]mask# 批量推理 - CANN自动适配batch sizeoutputself.inference.infer(input_ids_batch,mask_batch)# 分发结果fori,requestinenumerate(requests):result_queuerequest[2]result_queue.put(output[i])defstop(self):停止批处理器self.runningFalseself.worker.join()# 使用示例batcherCANNDynamicBatcher(inference,max_batch8)# 模拟多个并发请求importconcurrent.futuresdefsend_request(text_length):input_idsnp.random.randint(0,30000,text_length)attention_masknp.ones(text_length)resultbatcher.predict(input_ids,attention_mask)returnresult.shape# 并发发送请求withconcurrent.futures.ThreadPoolExecutor(max_workers10)asexecutor:futures[executor.submit(send_request,np.random.randint(64,256))for_inrange(20)]results[f.result()forfinfutures]print(f处理了{len(results)}个请求)batcher.stop()4.2 多模态动态shape在多模态模型中不同模态的输入shape可能都是动态的。CANN支持多输入的动态shape# 导出多模态模型图像文本dynamic_axes{image:{0:batch_size,2:height,3:width},# 图像动态batch和分辨率text:{0:batch_size,1:seq_length},# 文本动态batch和长度output:{0:batch_size}}torch.onnx.export(multimodal_model,(sample_image,sample_text),multimodal_dynamic.onnx,dynamic_axesdynamic_axes,opset_version14)# ATC转换时指定多个动态维度# atc --modelmultimodal_dynamic.onnx \# --input_shapeimage:-1,3,-1,-1;text:-1,-1 \# --dynamic_dims1,224,224,128;1,384,384,256;4,224,224,128 \# ...五、动态shape最佳实践5.1 何时使用动态shape适合使用动态shape的场景输入尺寸变化范围大且不可预测需要支持多种输入尺寸但不想维护多个模型服务端部署需要动态批处理不适合使用动态shape的场景输入尺寸固定或变化很小对首次推理延迟要求极高动态shape首次需要编译边缘设备资源受限动态shape需要更多内存5.2 性能调优建议合理设置档位根据实际数据分布设置档位避免过多或过少预热充分在正式服务前对所有档位进行预热触发shape缓存监控shape分布记录实际推理中各shape的出现频率优化档位设置批处理优化尽量使用批处理提高硬件利用率# 预热所有档位defwarmup_all_gears(inference,gear_shapes,iterations10):预热所有档位print(开始预热所有档位...)forbatch,seq_leningear_shapes:print(f 预热 ({batch},{seq_len})...)input_idsnp.random.randint(0,30000,(batch,seq_len))attention_masknp.ones((batch,seq_len))for_inrange(iterations):_inference.infer(input_ids,attention_mask)print(预热完成所有档位已缓存)# 在服务启动时预热gear_shapes[(1,128),(1,256),(4,128),(4,256)]warmup_all_gears(inference,gear_shapes)5.3 故障排查如果遇到动态shape相关问题可以按以下步骤排查检查档位设置确认实际输入shape是否在档位范围内查看日志CANN会输出详细的shape匹配日志验证模型导出确认ONNX模型的动态维度设置正确测试边界情况测试最小和最大shape是否正常工作总结CANN的动态shape功能为处理可变输入提供了高效的解决方案。通过本文的学习我们掌握了动态shape的应用场景和优势如何导出和转换动态shape模型动态shape推理的实现方法性能优化技巧包括档位设计和shape缓存高级应用场景如自适应批处理和多模态支持在实际应用中建议根据业务特点选择合适的档位配置并通过充分预热来获得最佳性能。动态shape虽然增加了一些复杂度但带来的灵活性和资源节省是非常值得的。