博客算法工具链用hb_perf和Trace给征程6模型做性能解剖:从45ms到28ms的优化实录

用hb_perf和Trace给征程6模型做性能解剖:从45ms到28ms的优化实录

默认265282026-08-30
23
0

模型部署到板端之后,"能跑"只是第一步,"跑得快"才是产品的要求。地平线提供了一套性能分析工具,可以详细记录模型在BPU上的每一层耗时、内存占用、算子执行顺序。这篇结合一个真实项目的Profiling过程,讲讲怎么用这些数据做优化决策,顺便分享几个我用这些工具时踩的坑。

一、工具链的性能分析工具全家桶

地平线工具链提供了三个核心的性能分析工具:

1. hb_perf:生成详细的性能报告(HTML格式),看每层延迟和内存

2. hb_trace:抓取BPU执行trace,看算子级别的执行顺序和core分配

3. hrt_model_exec:标准化的性能测试工具,输出延迟分布和利用率

我一般的用法是先用hb_trace抓数据,再用hb_perf生成可视化报告,最后用hrt_model_exec做基准对比。

二、hb_trace的基本用法

```bash

# 确认trace工具可用

which hb_trace

# /usr/bin/hb_trace

# 对hbm模型做trace

hb_trace --model ./model.hbm \

--input ./input_640x640.bin \

--output trace_result.json \

--trace-level full

```

--trace-level有几个选项:

· basic:只记录每层总耗时

· full:记录每层耗时 + 算子详细信息 + 内存分配

· memory:重点关注内存分配和复用

· pipeline:记录BPU双核的流水线执行情况

我一般直接用full,信息最全,后续分析不用重跑。

注意一个坑:

trace抓数据的时候,如果输入bin文件的尺寸和模型期望的不一致,工具不会报错,但抓出来的数据是错的。我有一次用640x640的模型但输入bin是512x512,trace结果里所有层的延迟都异常低(因为实际只处理了一部分数据),排查了半天才发现是输入尺寸的问题。

三、Trace结果的结构解读

Trace输出是JSON格式,结构大概这样:

```json

{

"model_info": {

"model_name": "yolov5m",

"input_shape": [1, 3, 640, 640],

"output_shape": [1, 25200, 85],

"total_layers": 157

},

"execution_trace": [

{

"layer_id": 0,

"layer_name": "conv_stem",

"op_type": "Conv2d",

"input_shape": [1, 3, 640, 640],

"output_shape": [1, 32, 320, 320],

"kernel_size": [6, 6],

"stride": [2, 2],

"timing": {

"bpu_cycles": 420000,

"total_us": 2100,

"start_time": 0,

"end_time": 2100

},

"memory": {

"input_size_bytes": 1228800,

"output_size_bytes": 13107200,

"weight_size_bytes": 6912

},

"core_id": 0,

"pipeline_stage": 0

}

],

"summary": {

"total_bpu_cycles": 85000000,

"total_time_us": 45000,

"bpu_utilization": 76.5,

"memory_peak_mb": 142.3

}

}

```

关键字段:

字段 含义 优化关注点

bpu_cycles BPU实际执行周期 越大说明计算量越大

total_us 实际耗时 定位最耗时的层

core_id 执行该层的BPU core 看负载是否均衡

pipeline_stage 流水线阶段 看双核流水线利用

memory_peak_mb 内存峰值 是否超板端DDR

四、Profiling分析实战

我分析了一个检测模型,总延迟45ms,帧率22fps,目标是做到30fps(延迟

步骤1:找出Top耗时层

写了个Python脚本解析trace:

```python

import json

with open('trace_result.json') as f:

trace = json.load(f)

layers = trace['execution_trace']

sorted_layers = sorted(layers, key=lambda x: x['timing']['total_us'], reverse=True)

print("Top 10最耗时层:")

for i, layer in enumerate(sorted_layers[:10]):

print(f"{i+1}. {layer['layer_name']}: {layer['timing']['total_us']/1000:.2f}ms "

f"({layer['op_type']}, out={layer['output_shape']})")

```

输出:

```

Top 10最耗时层:

1. backbone_stage4_conv3: 8.45ms (Conv2d, out=[1,512,20,20])

2. neck_pan_conv1: 6.23ms (Conv2d, out=[1,256,40,40])

3. backbone_stage3_conv2: 5.67ms (Conv2d, out=[1,256,40,40])

4. detect_head_cls: 4.12ms (Conv2d, out=[1,80,80,80])

5. backbone_stage4_conv1: 3.89ms (Conv2d, out=[1,512,20,20])

```

前3层加起来20.35ms,占总延迟45%。优化这几层收益最大。

步骤2:分析为什么慢

看第1层backbone_stage4_conv3,输出shape是[1,512,20,20],kernel=3x3, stride=1。512通道的大卷积在20x20的feature map上跑。

这一层的bpu_cycles是1200万,而同计算量的另一层backbone_stage3_conv2(256通道,40x40 feature map)只有450万cycles。512通道的卷积虽然FLOPs只多了50%,但cycles多了167%。

原因是512太大了,BPU的SRAM装不下全部weight,需要分多次从DDR加载,每次加载之间要等待数据传输。这是典型的内存带宽瓶颈,不是计算瓶颈。

步骤3:针对性优化

方向A:降通道数

如果512通道对精度不是必须的,用bottleneck结构降通道:

```python

# 原始

nn.Conv2d(512, 512, 3, padding=1)

# 优化

nn.Sequential(

nn.Conv2d(512, 256, 1), # 1x1降通道

nn.Conv2d(256, 256, 3, padding=1), # 3x3计算

nn.Conv2d(256, 512, 1), # 1x1升通道

)

```

bottleneck结构虽然多了两层1x1卷积,但3x3卷积的通道从512降到了256,weight加载量减少了一半。实测延迟从8.45ms降到了4.2ms,精度掉0.4%。

方向B:调整weight缓存策略

在编译配置里调weight_cache_strategy:

```yaml

compiler_parameters:

optimize_level: O3

core_num: 2

pipeline: true

memory_parameters:

weight_cache_strategy: round_robin

weight_prefetch: true

```

round_robin下,工具链把weight轮流加载到两个core的SRAM,减少单个core的等待。实测backbone_stage4_conv3的延迟从8.45ms降到了6.8ms。

步骤4:CPU fallback层排查

Trace里core_id=-1的层表示fallback到CPU执行。我项目里有3个:

· focus_slice:slice操作,BPU不支持

· upsample_nearest:3x上采样,BPU只支持2x/4x/8x

· concat_3:3路tensor拼接,shape不一致时BPU不支持

focus_slice解决:替换成stride=2的6x6卷积

```python

# 原始Focus层

class Focus(nn.Module):

def forward(self, x):

return torch.cat([

x[..., ::2, ::2],

x[..., 1::2, ::2],

x[..., ::2, 1::2],

x[..., 1::2, 1::2]

], dim=1)

# 替换

self.stem = nn.Conv2d(3, 32, 6, stride=2, padding=2)

```

省了7ms。

upsample_nearest解决:改用转置卷积

```python

# 原始:3x上采样(fallback到CPU)

F.interpolate(x, scale_factor=3, mode='nearest')

# 替换:转置卷积

nn.ConvTranspose2d(channels, channels, 2, stride=3, padding=1)

```

省了5ms。

五、hb_perf生成可视化报告

```bash

hb_perf --model yolov5m.hbm --output perf_report.html

```

生成的HTML报告包含:

· 每层延迟的柱状图

· 内存占用的堆叠图

· BPU core利用率曲线

· pipeline stage的执行时序图

一个小技巧: 报告里有"Layer Dependency Graph",可以看到算子之间的数据依赖关系。如果发现有大量的cross-core数据搬运(从一个core的输出直接是另一个core的输入),说明pipeline没有优化好,可以尝试调整编译参数或者模型结构来减少跨core搬运。

六、优化前后的完整对比

经过三轮优化:

指标 优化前 优化后 提升

总延迟 45ms 28ms -38%

帧率 22fps 35.7fps +62%

BPU利用率 76% 91% +15%

CPU fallback层数 3 0 -3

内存峰值 142MB 108MB -24%

mAP 82.1% 81.7% -0.4%

帧率从22fps干到35.7fps,超过30fps目标。精度只掉0.4%,在可接受范围。

七、hrt_model_exec的标准化测试

社区里有人推荐用hrt_model_exec做标准化的性能基准:

```bash

hrt_model_exec --model yolov5m.hbm \

--input input_640x640.bin \

--thread-num 1 \

--frame-count 100 \

--warmup-frame 10 \

--output perf.json

```

这个工具会输出:

· 平均延迟

· P50/P90/P99延迟(百分位延迟)

· BPU利用率

· DDR带宽占用

重点看P99延迟。

平均延迟28ms看着不错,但P99可能有42ms。在自动驾驶场景里,最坏情况比平均情况更关键。建议产品验收时以P90或P99为准。

顺便提一嘴,hrt_model_exec的--thread-num参数不要设超过BPU core数。征程6M是双核BPU,设2就够了,设4反而因为线程切换变慢。

八、自定义Trace点

如果模型里有自定义的C++后处理,可以手动插入trace点:

```cpp

#include

void my_nms(float* boxes, int num) {

HB_TRACE_BEGIN("custom_nms");

// ... NMS逻辑

HB_TRACE_END("custom_nms");

}

```

自定义trace点会出现在最终的trace报告里,帮助分析后处理的耗时。我项目里的NMS后处理最开始占8ms,优化了IOU计算(用SIMD加速)之后降到了2.5ms。

九、注意事项总结

1. 先用checker预检查:比等编译报错再排查效率高10倍。

2. trace抓数据时确认输入尺寸:输入bin尺寸不对会导致trace数据全错。

3. Top耗时层决定总延迟:前20%的层通常占延迟的60-70%。

4. 区分计算瓶颈和内存瓶颈:cycles高但shape小,大概率是weight加载瓶颈。

5. CPU fallback层逐个消除:替换优于fallback,性能差距10-50倍。

6. 看P99不要只看平均:长尾延迟才是产品稳定性的杀手。

7. 线程数不超过core数:J6M双核,最多2线程做推理。

8. 自定义trace点分析后处理:HB_TRACE_BEGIN/END插到关键函数里。

9. pipeline优化对多层小卷积效果最好:1x1卷积堆叠的bottleneck结构收益最大。

10. 跨core数据搬运要关注:hb_perf报告里的dependency graph会暴露这个问题。

算法工具链
社区征文征程6
评论0
0/600