Android端Mali GPU推理结果异常
简述
- Android端在使用gpu进行推理的过程中,发现Mail的gpu推理结果出错,而晓龙的Adreno gpu推理结果正常
detail | 详细描述 | 詳細な説明
- ncnn,使用官方提供的ncnn-20241226-android-vulkan,或者自行根据源码进行编译
- 模型 yolov11-obb模型,没经过魔改
- 模型参数(部分)
7767517
311 373
Input in0 0 1 in0
Convolution conv_0 1 1 in0 1 0=16 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=432
Swish silu_93 1 1 1 2
Convolution conv_1 1 1 2 3 0=32 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=4608
Swish silu_94 1 1 3 4
Convolution conv_2 1 1 4 5 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=1024
Swish silu_95 1 1 5 6
Slice split_0 1 2 6 7 8 -23300=2,16,16 1=0
Split splitncnn_0 1 3 8 9 10 11
Convolution conv_3 1 1 11 12 0=8 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152
Swish silu_96 1 1 12 13
Convolution conv_4 1 1 13 14 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152
Swish silu_97 1 1 14 15
BinaryOp add_0 2 1 10 15 16 0=0
Concat cat_0 3 1 7 9 16 17 0=0
Convolution conv_5 1 1 17 18 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=3072
Swish silu_98 1 1 18 19
Convolution conv_6 1 1 19 20 0=64 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=36864
Swish silu_99 1 1 20 21
Convolution conv_7 1 1 21 22 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
Swish silu_100 1 1 22 23
Slice split_1 1 2 23 24 25 -23300=2,32,32 1=0
Split splitncnn_1 1 3 25 26 27 28
Convolution conv_8 1 1 28 29 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=4608
Swish silu_101 1 1 29 30
Convolution conv_9 1 1 30 31 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=4608
Swish silu_102 1 1 31 32
BinaryOp add_1 2 1 27 32 33 0=0
Concat cat_1 3 1 24 26 33 34 0=0
Convolution conv_10 1 1 34 35 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=12288
Swish silu_103 1 1 35 36
Split splitncnn_2 1 2 36 37 38
Convolution conv_11 1 1 38 39 0=128 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=147456
...
Reshape reshape_182 1 1 132 140 0=20 1=20 2=128
ConvolutionDepthWise convdw_199 1 1 140 141 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 7=128
BinaryOp add_7 2 1 139 141 142 0=0
Convolution conv_35 1 1 142 143 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384
BinaryOp add_8 2 1 125 143 144 0=0
Split splitncnn_15 1 2 144 145 146
Reshape view_188 1 1 272 273 0=400 1=1
Concat cat_17 3 1 261 267 273 274 0=1
Sigmoid sigmoid_178 1 1 274 275
BinaryOp sub_15 1 1 275 276 0=1 1=1 2=2.500000e-01
BinaryOp mul_16 1 1 276 277 0=2 1=1 2=3.141593e+00
Split splitncnn_27 1 3 277 278 279 280
Convolution conv_71 1 1 192 281 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864
Swish silu_158 1 1 281 282
Convolution conv_72 1 1 282 283 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864
Swish silu_159 1 1 283 284
Convolution conv_73 1 1 284 285 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
ConvolutionDepthWise convdw_200 1 1 195 286 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64
Swish silu_160 1 1 286 287
Convolution conv_74 1 1 287 288 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
Swish silu_161 1 1 288 289
ConvolutionDepthWise convdw_201 1 1 289 290 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64
Swish silu_162 1 1 290 291
Convolution conv_75 1 1 291 292 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
Swish silu_163 1 1 292 293
Convolution conv_76 1 1 293 294 0=7 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=448
Concat cat_18 2 1 285 294 295 0=0
Convolution conv_77 1 1 214 296 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=73728
Swish silu_164 1 1 296 297
Convolution conv_78 1 1 297 298 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864
Swish silu_165 1 1 298 299
Convolution conv_79 1 1 299 300 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
ConvolutionDepthWise convdw_202 1 1 217 301 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 7=128
Swish silu_166 1 1 301 302
Convolution conv_80 1 1 302 303 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=8192
Swish silu_167 1 1 303 304
ConvolutionDepthWise convdw_203 1 1 304 305 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64
Swish silu_168 1 1 305 306
Convolution conv_81 1 1 306 307 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
Swish silu_169 1 1 307 308
Convolution conv_82 1 1 308 309 0=7 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=448
Concat cat_19 2 1 300 309 310 0=0
Convolution conv_83 1 1 252 311 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=147456
Swish silu_170 1 1 311 312
Convolution conv_84 1 1 312 313 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864
Swish silu_171 1 1 313 314
Convolution conv_85 1 1 314 315 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
ConvolutionDepthWise convdw_204 1 1 254 316 0=256 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 7=256
Swish silu_172 1 1 316 317
Convolution conv_86 1 1 317 318 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384
Swish silu_173 1 1 318 319
ConvolutionDepthWise convdw_205 1 1 319 320 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64
Swish silu_174 1 1 320 321
Convolution conv_87 1 1 321 322 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096
Swish silu_175 1 1 322 323
Convolution conv_88 1 1 323 324 0=7 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=448
Concat cat_20 2 1 315 324 325 0=0
Reshape view_189 1 1 295 326 0=6400 1=71
Reshape view_190 1 1 310 327 0=1600 1=71
Reshape view_191 1 1 325 328 0=400 1=71
Concat cat_21 3 1 326 327 328 329 0=1
Slice split_10 1 2 329 330 331 -23300=2,64,7 1=0
Reshape view_192 1 1 330 332 0=8400 1=16 2=4
Permute transpose_198 1 1 332 333 0=2
Softmax softmax_181 1 1 333 334 0=0 1=1
Convolution conv_89 1 1 334 335 0=1 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=0 6=16
Reshape view_193 1 1 335 336 0=8400 1=4
MemoryData pnnx_fold_anchor_points.1 0 1 337 0=8400 1=2
Slice split_11 1 2 336 338 339 -23300=2,2,-233 1=0
Split splitncnn_29 1 2 339 340 341
Split splitncnn_28 1 2 338 342 343
UnaryOp cos_17 1 1 279 344 0=10
Split splitncnn_30 1 2 344 345 346
UnaryOp sin_18 1 1 280 347 0=9
Split splitncnn_31 1 2 347 348 349
BinaryOp sub_19 2 1 340 342 350 0=1
BinaryOp div_20 1 1 350 351 0=3 1=1 2=2.000000e+00
Slice split_12 1 2 351 352 353 -23300=2,1,-233 1=0
Split splitncnn_33 1 2 353 354 355
Split splitncnn_32 1 2 352 356 357
BinaryOp mul_21 2 1 354 348 358 0=2
BinaryOp mul_22 2 1 356 345 359 0=2
BinaryOp sub_23 2 1 359 358 360 0=1
BinaryOp mul_24 2 1 355 346 361 0=2
BinaryOp mul_25 2 1 357 349 362 0=2
BinaryOp add_26 2 1 362 361 363 0=0
Concat cat_22 2 1 360 363 364 0=0
BinaryOp add_27 2 1 364 337 365 0=0
BinaryOp add_28 2 1 343 341 366 0=0
Concat cat_23 2 1 365 366 367 0=0
Reshape reshape_183 1 1 255 368 0=8400 1=1
BinaryOp mul_29 2 1 367 368 369 0=2
Sigmoid sigmoid_179 1 1 331 370
Concat cat_24 2 1 369 370 371 0=0
Concat cat_25 2 1 371 278 out0 0=0
- 代码
_hasGPU = ncnn::get_gpu_count() > 0;
_Net->opt.use_fp16_arithmetic = false;
_Net->opt.use_fp16_storage = false;
_Net->opt.use_fp16_packed = false;
_Net->opt.use_vulkan_compute = _hasGPU; // 将其设置为false,推理结果均正常
_Net->opt.use_packing_layout=true;
// in_pad 的w, h 均为640
ncnn::Extractor ex = _Net->create_extractor();
ex.input("in0", in_pad);
ncnn::Mat out;
int extract_result = ex.extract("out0", out);
- 出错描述
- 抽取out0进行后处理时发现box的x,y,w,h出现异常,x,y远大于640,wh也远大于实际的box尺寸,但是confidence和angle是正常的
- 模型配置倒数第四行,单独抽取369发现x,y,w,h的计算结果依然异常,于是抽取367,368自行进行乘法运算得到结果是正常的(369box的位置,370置信度,278角度)
// 提取box
ncnn::Mat out_box;
int extract_result = ex.extract(367, out_box);
// 提取步长
ncnn::Mat out_stride;
ex.extract(368, out_stride);
// 提取置信度
ncnn::Mat out_conf;
ex.extract(370, out_conf);
- 出错设备
- huawei mate 40,gpu: Mali-G78
- vivo x100, x90系列 gpu: Mali-G715 Immortalis MC11
- 搭载骁龙Adreno的设备推理结果正常,禁用gpu推理,
_Net->opt.use_vulkan_compute = false;所有设备推理结果均正常
hi, ncnn yolo11 examples are on board https://github.com/Tencent/ncnn/tree/master/examples https://github.com/nihui/ncnn-android-yolo11
model conversion guide (zh) https://zhuanlan.zhihu.com/p/1903414797195781701
我也遇到了同样的问题,有的输出是对的,有的输出是错的。
hi, ncnn yolo11 examples are on board https://github.com/Tencent/ncnn/tree/master/examples https://github.com/nihui/ncnn-android-yolo11
model conversion guide (zh) https://zhuanlan.zhihu.com/p/1903414797195781701
首先感谢大佬,用例子中的方式裁剪模型,修改推理代码后,在原先出问题的GPU上输出结果正常了。
分析例子发现,外部实现后处理相当于将box位置的计算拿出来放在CPU上进行。令我不解的是,为什么后处理放在GPU上会运行错误。
后面我又进行了以下测试:
- 测试环境:同样放在出错的GPU上运行
- 代码1:
ncnn::Extractor ex = net->create_extractor();
ex.input("in0", in_pad);
ncnn::Mat out;
int extract_result = ex.extract("out0", out);
结果:第一次结果正常,后续结果全是异常的
- 代码2:
ncnn::Extractor ex = net->create_extractor();
ex.input("in0", in_pad);
// 这一块什么也不做,只单纯的extract
ncnn::Mat out_367;
ex.extract("367", out_367);
//////////////////////////
ncnn::Mat out;
int extract_result = ex.extract("out0", out);
结果:所有推理结果恢复正常(仅当我extract 367层时正常,extract其他层:369,370结果都异常)
感觉像是显存复用出了问题。
抱歉,刚才误触了关闭,这里直接说明最新进展:
错误原因
经排查,出现错误的GPU是因为其不支持Vulkan的推送描述符VK_KHR_push_descriptor:
vkdev->info.support_VK_KHR_push_descriptor();
错误情况
- box位置错误
- 概率、置信度、角度正常
错误定位
- 最终定位到错误出现在
layer_index = 204层,后续位置计算依赖该层结果,因此该层出错导致最终位置错误:layer_204->MemoryData pnnx_188 0 1 255 0=8400 - 影响204层出错的层大概率是305层:
layer_305->Concat cat_23 2 1 365 366 367 0=0
实验过程
实验一
在305层之前计算204层,并将204层结果加入blob_mats_gpu,结果恢复正常:
int NetPrivate::do_forward_layer(int dst_index, const Layer* layer, std::vector<VkMat>& blob_mats_gpu, VkCompute& cmd, const Option& opt) const
{
...// 前面保持不变
if (layer->name == "cat_23") {
NCNN_LOGE("cat_23 之前");
const Layer* layer_temp = layers[204];
std::vector<VkMat> top_blobs_temp(layer->tops.size());
std::vector<VkMat> bottom_blobs_temp(layer->bottoms.size());
layer_temp->forward(bottom_blobs_temp, top_blobs_temp, cmd, opt);
for (size_t i = 0; i < layer_temp->tops.size(); i++)
{
int top_blob_index = layer_temp->tops[i];
blob_mats_gpu[top_blob_index] = top_blobs_temp[i];
}
}
std::vector<VkMat> top_blobs(layer->tops.size());
int ret = layer->forward(bottom_blobs, top_blobs, cmd, opt);
if (ret != 0)
return ret;
....// 后面保持不变
}
实验二
在305层计算之后执行submit_and_wait,结果恢复正常:
int NetPrivate::do_forward_layer(int dst_index, const Layer* layer, std::vector<VkMat>& blob_mats_gpu, VkCompute& cmd, const Option& opt) const
{
...// 前面保持不变
std::vector<VkMat> top_blobs(layer->tops.size());
int ret = layer->forward(bottom_blobs, top_blobs, cmd, opt);
if (layer->name == "cat_23") {
cmd.submit_and_wait();
cmd.reset();
}
if (ret != 0)
return ret;
....// 后面保持不变
}
实验三
在MemoryData_vulkan forward clone操作之后加入cmd.record_download,结果恢复正常:
int MemoryData_vulkan::forward(const std::vector<VkMat>& /*bottom_blobs*/, std::vector<VkMat>& top_blobs, VkCompute& cmd, const Option& opt) const
{
VkMat& top_blob = top_blobs[0];
cmd.record_clone(data_gpu, top_blob, opt);
if (name == "pnnx_188") {
Mat temp1;
cmd.record_download("", data_gpu, temp1, opt);
}
if (top_blob.empty())
return -100;
return 0;
}
同时重载函数record_download
void VkCompute::record_download(std:string flag, const VkMat& src, Mat& dst, const Option& opt)
{
// resolve dst_elempack
int dims = src.dims;
int elemcount = 0;
if (dims == 1) elemcount = src.elempack * src.w;
if (dims == 2) elemcount = src.elempack * src.h;
if (dims == 3 || dims == 4) elemcount = src.elempack * src.c;
int dst_elempack = 1;
if (opt.use_packing_layout)
dst_elempack = elemcount % 4 == 0 ? 4 : 1;
else
dst_elempack = 1;
// gpu cast to fp32 on the fly (integrated gpu)
Option opt_staging = opt;
if (!opt_staging.blob_vkallocator->mappable)
{
opt_staging.blob_vkallocator = opt.staging_vkallocator;
}
int cast_type_to = 0;
if (vkdev->info.type() != 0)
{
cast_type_to = 1;
}
if (src.elemsize == src.elempack * 1u)
{
cast_type_to = 4;
}
VkMat dst_staging;
vkdev->convert_packing(src, dst_staging, dst_elempack, cast_type_to, *this, opt_staging);
// 后续代码删除
}
实验四
修改record_clone代码(固定增加内存屏障),结果恢复正常:
原代码:
void VkCompute::record_clone(const VkMat& src, VkMat& dst, const Option& opt)
{
// NCNN_LOGE("record_clone buffer to buffer");
// create dst
dst.create_like(src, opt.blob_vkallocator);
if (dst.empty())
return;
if (src.data->access_flags & VK_ACCESS_TRANSFER_WRITE_BIT || src.data->stage_flags != VK_PIPELINE_STAGE_TRANSFER_BIT)
{
// barrier device any @ compute to transfer-read @ compute
VkBufferMemoryBarrier* barriers = new VkBufferMemoryBarrier[1];
barriers[0].sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
barriers[0].pNext = 0;
barriers[0].srcAccessMask = src.data->access_flags;
barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barriers[0].buffer = src.buffer();
barriers[0].offset = src.buffer_offset();
barriers[0].size = src.buffer_capacity();
VkPipelineStageFlags src_stage = src.data->stage_flags;
VkPipelineStageFlags dst_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
if (vkdev->info.support_VK_KHR_push_descriptor())
{
vkCmdPipelineBarrier(d->compute_command_buffer, src_stage, dst_stage, 0, 0, 0, 1, barriers, 0, 0);
delete[] barriers;
}
else
{
VkComputePrivate::record r;
r.type = VkComputePrivate::record::TYPE_buffer_barrers;
r.command_buffer = d->compute_command_buffer;
r.buffer_barrers.src_stage = src_stage;
r.buffer_barrers.dst_stage = dst_stage;
r.buffer_barrers.barrier_count = 1;
r.buffer_barrers.barriers = barriers;
d->delayed_records.push_back(r);
}
// mark device transfer-read @ transfer
src.data->access_flags = VK_ACCESS_TRANSFER_READ_BIT;
src.data->stage_flags = VK_PIPELINE_STAGE_TRANSFER_BIT;
}
... // 后面保持不变
}
修改后代码:
void VkCompute::record_clone(const VkMat& src, VkMat& dst, const Option& opt)
{
// create dst
dst.create_like(src, opt.blob_vkallocator);
if (dst.empty())
return;
if (src.data->access_flags & VK_ACCESS_TRANSFER_WRITE_BIT || src.data->stage_flags != VK_PIPELINE_STAGE_TRANSFER_BIT)
{
// 若当前是COMPUTE阶段后的clone,自动补充COMPUTE同步
VkAccessFlags real_src_access = src.data->access_flags;
VkPipelineStageFlags real_src_stage = src.data->stage_flags;
if (!vkdev->info.support_VK_KHR_push_descriptor())
{
// 强制加入“计算写入”的同步(覆盖干扰)
real_src_access |= VK_ACCESS_SHADER_WRITE_BIT;
real_src_stage |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
}
VkBufferMemoryBarrier* barriers = new VkBufferMemoryBarrier[1];
barriers[0].sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
barriers[0].srcAccessMask = real_src_access; // 用扩展后的access
barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barriers[0].buffer = src.buffer();
barriers[0].offset = src.buffer_offset();
barriers[0].size = src.buffer_capacity();
VkPipelineStageFlags src_stage = real_src_stage; // 用扩展后的stage
VkPipelineStageFlags dst_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
if (vkdev->info.support_VK_KHR_push_descriptor())
{
NCNN_LOGE("gpu support_VK_KHR_push_descriptor!");
vkCmdPipelineBarrier(d->compute_command_buffer, src_stage, dst_stage, 0, 0, 0, 1, barriers, 0, 0);
delete[] barriers;
}
else
{
NCNN_LOGE("gpu does not support_VK_KHR_push_descriptor!");
VkComputePrivate::record r;
r.type = VkComputePrivate::record::TYPE_buffer_barrers;
r.command_buffer = d->compute_command_buffer;
r.buffer_barrers.src_stage = src_stage;
r.buffer_barrers.dst_stage = dst_stage;
r.buffer_barrers.barrier_count = 1;
r.buffer_barrers.barriers = barriers;
d->delayed_records.push_back(r);
}
// 注释掉这两行,
// src.data->access_flags = VK_ACCESS_TRANSFER_READ_BIT;
// src.data->stage_flags = VK_PIPELINE_STAGE_TRANSFER_BIT;
}
... // 后面保持不变
}
实验结论
- 首次推理正常,后续推理出错。
- 将204层
forward放在305层forward之前,结果恢复正常。 - 305层
forward后执行cmd.submit_and_wait(),结果恢复正常。 - 204层
forward后执行cmd.record_download(),结果恢复正常。 - 在
record_clone中固定增加内存屏障,结果恢复正常。
总结(AI)
核心根因总结
在不支持VK_KHR_push_descriptor的GPU上,ncnn需将所有GPU指令(如record_pipeline、record_clone)存入delayed_records延迟队列,等待submit_and_wait()统一提交执行。该机制存在**“跨推理的资源状态累积”** 与**“管线阶段同步缺失”** 两大缺陷:
-
状态累积:首次推理后,
data_gpu等资源的“访问标志(access_flags)”“管线阶段(stage_flags)”等状态未被完全重置,残留至后续推理; -
同步缺失:延迟队列仅保证“CPU提交顺序”,不保证“GPU执行顺序与资源可见性”,且
record_clone等操作的内置屏障因状态残留而失效,无法约束305层(计算阶段)与204层(传输阶段)的执行关系。
最终导致:204层clone操作因“资源状态误判”“管线阶段未同步”读取到异常数据,表现为后续推理结果错误。
关键现象的技术解析
1. 首次推理正常,后续异常
-
首次正常的本质:
初始化时,data_gpu通过upload_model上传至GPU,ncnn会自动插入显式内存屏障(确保CPU写入的数据被GPU可见),此时data_gpu的状态为VK_ACCESS_HOST_WRITE_BIT(主机写入)+VK_PIPELINE_STAGE_HOST_BIT(主机阶段),状态干净;
延迟队列是“全新创建+一次性执行”,submit_and_wait()按顺序跑完所有指令后,清空队列并重置命令缓冲,无状态残留,clone操作可正常读取data_gpu。 -
后续异常的根源:
首次推理后,record_clone会永久修改data_gpu的状态(改为VK_ACCESS_TRANSFER_READ_BIT+VK_PIPELINE_STAGE_TRANSFER_BIT),导致后续推理中:-
clone的内置屏障因“状态条件不满足”而失效(无法触发跨阶段同步); - 305层的计算指令(
COMPUTE阶段)与204层的clone指令(TRANSFER阶段)无同步约束,GPU可能重排序或状态误判,导致clone读取data_gpu时的“管线阶段权限”“内存可见性”异常。
-
2. 204层放305层前执行,结果正常
-
逻辑链:延迟队列顺序变为
[204层clone → 305层concat],利用了GPU的**“短任务优先调度策略”**:- 204层
clone是“传输操作”(计算量极小、执行时间短),会被GPU优先调度执行,在305层concat(计算操作,耗时久)开始前就完成数据拷贝,获取到data_gpu的“干净状态”; - 后续305层的
concat虽会干扰GPU的全局状态,但clone的结果(top_blobs[0])已生成且不再被修改,因此不受影响。
- 204层
-
关键澄清:305层并未“污染204层的结果”,而是
clone提前执行规避了后续的状态干扰。
3. 305层后执行cmd.submit_and_wait(),结果正常
-
submit_and_wait()的完整作用(不止“等待执行”):- 强制执行延迟队列:立即将305层的所有计算指令提交至GPU并等待执行完成,消除“未提交指令的状态残留”;
-
显式同步内存状态:通过
vkWaitForFences确保GPU的计算结果已刷入内存,管线阶段重置为“空闲”; -
清空队列与重置缓冲:执行后
delayed_records.clear(),并重置命令缓冲的隐性状态,相当于“人为初始化”,后续204层clone可触发正常的同步逻辑。
- 本质:通过主动提交消除“状态累积”,修复同步缺失。
上述总结来源于AI
实验虽然恢复正常,但是不够通用,性能方面也有折扣
这属于vulkan的bug吗?这种现象有方式解决吗?
@nihui 大佬,我后续又深入vulkan研究了一下,麻烦你看看这种现象
只要存在为临时变量top_blob_unpacked申请内存,并且不支持VK_KHR_push_descriptor的GPU感觉都会必然出现这种现象。
一、RenderDoc捕获vulkan指令
通过RenderDoc捕获Vulkan指令,发现204层(clone运算)与305层(concat运算)存在显存地址重叠,最终导致依赖204层结果的306层读取异常。
1. 显存地址重叠验证
核心冲突对象为buffer5271,两层对该buffer的写入地址范围完全重叠,具体如下:
- 204层(clone传输单元):写入地址
3069888 ~ 3069888 + 33600
- 305层(CS计算单元):写入地址
3069888 ~ 3069888 + 134400
- 整体地址重叠:两层写入起始地址均为
3069888,204层地址范围完全包含于305层地址范围内。
2. 指令调度异常与数据覆盖
- 关键指令关联:204层对应指令
1719,305层对应指令1718,306层(依赖层)对应指令1725。 - 异常根源:
1718与1719之间无显式同步屏障,GPU为优化性能触发「指令重排」或「并行运算」。 - 数据覆盖过程:
- 1719(204层,传输操作)执行更快,先向buffer5271写入数据;
- 1718(305层,CS计算操作)后执行,覆盖buffer5271中已写入的1719数据;
- 1725(306层)依赖1719的原始数据,最终读取到被覆盖的305层数据,导致结果异常。
二、源码定位:ncnn Concat_vulkan层内存分配分析
通过分析ncnn源码中Concat_vulkan::forward函数,定位305层(concat运算)的内存分配与释放逻辑,明确其与204层共享内存的根源。
1. 核心源码片段(305层concat运算)
int Concat_vulkan::forward(const std::vector<VkMat>& bottom_blobs, std::vector<VkMat>& top_blobs, VkCompute& cmd, const Option& opt) const
{
// ...(省略无关逻辑)
if (dims == 2 && positive_axis == 0)
{
// ...(省略参数计算逻辑)
int out_elempack = top_h % 4 == 0 ? 4 : 1;
size_t out_elemsize = elemsize / elempack * out_elempack;
VkMat& top_blob = top_blobs[0];
// 1. 初始化top_blob,占用地址:0xb40000744ea042a0[+2935488] ========
top_blob.create(w, top_h / out_elempack, out_elemsize, out_elempack, opt.blob_vkallocator);
if (top_blob.empty())
return -100;
VkMat top_blob_unpacked = top_blob;
// 初始时top_blob_unpacked与top_blob共享地址:0xb40000744ea042a0[+2935488] ========
if (elempack < out_elempack)
{
// 2. 重新分配top_blob_unpacked,占用地址:0xb40000744ea042a0[+3069888] ========
top_blob_unpacked.create(w, top_h / elempack, elemsize, elempack, opt.workspace_vkallocator);
if (top_blob_unpacked.empty())
return -100;
}
// ...(后续计算逻辑)
}
// ...(函数返回)
}
2. 305层与204层内存占用时序
通过跟踪两层forward函数前后的buffer状态,明确内存复用过程:
(1)305层(concat)内存时序
-
forward前:空闲buffer为
0xb40000744ea042a0,地址范围2935488 ~ 15603712(大小12668224); -
forward中:
-
top_blob占用0xb40000744ea042a0[+2935488]; -
top_blob_unpacked重新分配后,占用0xb40000744ea042a0[+3069888];
-
-
forward后:
top_blob_unpacked占用的0xb40000744ea042a0[+3069888]被释放,回归空闲状态。
(2)204层(clone)内存时序
-
forward前:空闲buffer包含
0xb40000744ea042a0[+3069888](即305层释放的地址); -
forward后:204层复用该空闲地址,占用
0xb40000744ea042a0[+3069888],最终与305层形成地址重叠。
三、问题总结
1. 核心结论
306层读取异常的根本原因是305层与204层共享同一片显存地址+无显式同步屏障:GPU指令重排导致204层数据被305层覆盖,而306层依赖204层原始数据,最终引发错误。
2. 冲突根因拆解
1. 内存共享的原因(CPU视角)
305层计算时会临时占用内存地址A,运算结束后地址A被释放;204层计算时检测到地址A空闲,便复用该地址存储自身结果,最终导致305层与204层共享同一片内存。
2. 并行冲突的原因(GPU视角)
GPU调度器以性能优化为目标,当两层无显式同步屏障时,会允许二者并行执行或调整指令顺序,导致两层同时对地址A进行“写操作”,最终数据覆盖引发错误。
3. 设备差异补充
| 设备类型 | 关键特性 | 表现差异 |
|---|---|---|
| 正常设备 | 支持VK_KHR_push_descriptor |
GPU与CPU内存管理同步,即使存在地址重叠,也可避免异步时序错配; |
| 异常设备 | 不支持上述特性 | CPU内存释放时序与GPU异步执行时序不匹配,叠加内存复用,直接触发数据覆盖; |