【LLM推理与Serving⑨】业务模型推理引擎开发:以BEV自动驾驶为例的专用推理实践
系列文章索引:本系列从基础概念到工程实践,系统梳理LLM推理与Serving的核心知识。本文为第9篇,跳出通用LLM框架,看看业务专用推理引擎是怎么打造的。
一、开篇:通用LLM Serving之外,业务专用推理引擎有什么不同?
前面的文章我们一直在聊通用LLM推理框架------vLLM、TGI、TensorRT-LLM......这些框架有一个共同点:它们服务的是"大语言模型"这种相对标准化的模型结构。
但在真实的产业界,除了大语言模型,还有大量业务专用模型,它们的推理场景和通用LLM完全不同:
- 自动驾驶的BEV感知模型,输入是6路摄像头图像,输出是鸟瞰图下的障碍物检测
- 推荐系统的排序模型,输入是用户+物品的稀疏特征,输出是点击概率
- 金融风控模型,输入是几百维的结构化特征,输出是风险评分
- 工业质检的视觉模型,输入是高分辨率工业相机图像,输出是缺陷分类和定位
这些场景下,直接用通用推理框架往往效果不好 ------要么性能不够,要么功能不匹配,要么成本太高。于是就有了"业务专用推理引擎"的需求:针对特定业务场景的模型结构和硬件环境,做深度定制化的推理优化。
这篇文章我们就以**自动驾驶中的BEV感知 + 规划模型(PlanNN)**为例,讲讲专用推理引擎的开发方法论。为什么选这个例子?因为它足够典型:输入输出形态特殊、延迟要求极高、失败代价极大、硬件环境受限------这些都是专用推理引擎要面对的核心挑战。
如果你不是做自动驾驶的也没关系,本文讲的方法论是通用的:如何分析计算热点、如何设计引擎架构、如何做算子定制、如何做流水线优化、如何做算法-硬件协同------这些思路可以迁移到任何专用推理场景。
二、BEV/PlanNN与通用LLM Serving的差异
在讲怎么做之前,我们先搞清楚"为什么不能直接用通用框架"。BEV/PlanNN推理和LLM推理的差异,可以说是全方位的:
2.1 输入输出形态
LLM推理:
- 输入:一段token序列(离散的、变长的)
- 输出:一个一个生成的token(自回归、流式)
- 形状:输入长度不固定,从几个token到几千个token都有
BEV感知:
- 输入:6路(或更多)摄像头图像 + 相机内外参 + 车身姿态
- 输出:BEV视角下的3D检测框、车道线、分割图等
- 形状:输入基本固定(6×3×H×W),输出结构也固定
PlanNN规划模型:
- 输入:BEV特征 + 历史轨迹 + 导航信息
- 输出:未来轨迹点、加速度、转向角等控制信号
- 形状:高度结构化、定长
2.2 延迟要求
LLM推理:
- 首Token延迟(TTFT):几百毫秒到几秒都能接受
- 每Token延迟(TPOT):几十毫秒级别,人眼感觉流畅就行
- 总延迟:宽松,几秒到几十秒都正常
自动驾驶推理:
- 端到端延迟必须在几十毫秒以内(比如100ms以内,否则车都开出去好几米了)
- 延迟抖动(Jitter)必须极小,不能忽快忽慢
- 是硬实时要求,不是"越快越好",而是"必须在deadline内完成"
2.3 失败成本
LLM推理:
- 输出错误:最多用户体验不好,重试一下
- 服务挂了:用户等一等,或者切到备用服务
- 容错空间大
自动驾驶推理:
- 漏检一个障碍物:可能就是一场事故
- 推理超时:车来不及反应,后果不堪设想
- 失败成本极高,是安全关键系统(Safety-Critical)
2.4 硬件环境
LLM推理:
- 数据中心的高端GPU(A100、H100)
- 显存大(80GB+)、算力强
- 有稳定的供电和散热
- 可以随时升级硬件
车载推理:
- 车规级芯片(Orin、Thor、昇腾、地平线等)
- 算力有限(几十TOPS到几百TOPS),显存有限
- 功耗受限(几十瓦级别)
- 硬件一旦上车就很难换,软件要适配好几年
2.5 优化目标
LLM推理优化目标:
- 最大化吞吐(tokens/s)
- 兼顾延迟
- 在吞吐和延迟之间做权衡
BEV/PlanNN优化目标:
- 最小化延迟(确保在deadline内完成)
- 确定性(延迟抖动要小)
- 功耗控制
- 功能安全
一句话总结:LLM推理是"吞吐量优先"的批量处理,BEV推理是"延迟优先"的实时处理。 这两种场景的优化思路完全不同。
三、BEV/PlanNN计算热点分析
做推理优化的第一步,不是上来就写CUDA kernel,而是先搞清楚时间都花在哪儿了。也就是Profiling(性能剖析)。
3.1 典型BEV感知流水线
一个典型的BEV感知+规划流水线长这样:
多路摄像头图像
│
▼
┌─────────────┐
│ Image Backbone │ ← 图像特征提取(ResNet / ViT / ConvNeXt等)
└──────┬──────┘
│ 多视角图像特征
▼
┌─────────────┐
│ View Transform │ ← 视角变换:从图像视角转到BEV视角
└──────┬──────┘ (LSS / BEVFormer / Instant NGP等方法)
│ BEV特征
▼
┌─────────────┐
│ BEV Encoder │ ← BEV特征编码(卷积、注意力等)
└──────┬──────┘
│ 增强后的BEV特征
▼
┌─────────────┐
│ Temporal Fusion│ ← 时序融合(融合历史帧的BEV特征)
└──────┬──────┘
│ 时空融合后的BEV特征
▼
┌─────────────┐
│ Detection Head │ ← 检测头(3D检测、车道线、分割等)
└──────┬──────┘
│ 感知结果
▼
┌─────────────┐
│ PlanNN / Planner │ ← 规划网络(轨迹预测、路径规划)
└──────┬──────┘
│ 规划轨迹
▼
车辆控制
3.2 五大计算热点
根据实践经验,BEV感知流水线中,计算量主要集中在这几个地方:
1. Image Backbone(图像骨干网络)
- 占比:约30%~50%
- 做什么:对每一路摄像头图像做特征提取
- 特点:计算量大,但结构相对标准(CNN或Transformer),容易优化
- 优化空间:中等,因为CNN/Transformer的优化已经很成熟了
2. View Transform(视角变换)
- 占比:约20%~40%
- 做什么:把图像视角的特征"投射"到BEV视角
- 特点:是BEV特有的操作,形状不规则,访存模式复杂
- 优化空间:大,这是BEV推理优化的重点和难点
3. BEV Encoder(BEV编码器)
- 占比:约10%~20%
- 做什么:在BEV平面上做特征提取和增强
- 特点:通常是2D卷积或Deformable Attention
- 优化空间:中等,卷积优化成熟,但Deformable Attention优化空间大
4. Temporal Fusion(时序融合)
- 占比:约5%~15%
- 做什么:把历史帧的BEV特征和当前帧融合
- 特点:涉及对齐(warp)和融合操作
- 优化空间:中等,warp操作的访存优化是重点
5. PlanNN(规划网络)
- 占比:约5%~15%
- 做什么:基于感知结果生成规划轨迹
- 特点:通常是MLP或轻量Transformer,计算量不大但延迟要求高
- 优化空间:相对小,但对确定性要求高
python
# 伪代码:BEV感知推理的Profiling结果示例
# (数据为示意,实际因模型和硬件而异)
profile_result = {
"total_latency_ms": 85.3,
"breakdown": [
{"name": "Image Backbone", "latency_ms": 32.5, "percentage": "38.1%"},
{"name": "View Transform", "latency_ms": 28.7, "percentage": "33.6%"},
{"name": "BEV Encoder", "latency_ms": 12.3, "percentage": "14.4%"},
{"name": "Temporal Fusion", "latency_ms": 6.8, "percentage": "8.0%"},
{"name": "Detection Head", "latency_ms": 3.2, "percentage": "3.8%"},
{"name": "PlanNN", "latency_ms": 1.8, "percentage": "2.1%"},
],
"recommendation": "重点优化 View Transform 和 Image Backbone,合计占比70%+",
}
优化的黄金法则 :把精力花在占比最大的地方。 一个占比30%的模块,优化10%就能让整体快3%;一个占比5%的模块,就算优化50%,整体也只快2.5%。
所以第一步永远是Profiling,找到真正的瓶颈------不要凭感觉优化。
四、专用推理引擎架构设计
找到了热点之后,怎么组织这些模块?专用推理引擎的架构设计有哪些原则?
4.1 设计原则
1. 静态化(Static Everything)
- 输入输出形状、batch size、内存分配......能静态的尽量静态
- 为什么?动态意味着运行时开销,也意味着不确定性
- LLM推理追求动态(变长输入、动态batch),但实时嵌入式推理追求静态
2. 预分配(Pre-allocation)
- 所有内存(包括中间张量)都在初始化阶段分配好
- 推理过程中绝对不做动态内存分配
- 为什么?动态分配有延迟,而且可能失败(OOM)
3. 零拷贝(Zero Copy)
- 模块之间尽量传递指针/引用,不复制数据
- 输入输出缓冲区复用,减少内存搬运
- 为什么?内存拷贝的开销往往被低估,尤其是在嵌入式平台
4. 确定性(Determinism)
- 相同输入必须产生相同输出(包括数值精度)
- 延迟抖动必须控制在极小范围内
- 为什么?功能安全要求,以及下游模块的稳定性
5. 可回退(Fallback)
- 每个关键模块都要有降级方案
- 主路径超时了怎么办?有简化版的模型可以顶上
- 为什么?安全关键系统必须有容错机制
6. 可观测(Observability)
- 每个模块的延迟、输出统计、异常情况都要能监控
- 出了问题能快速定位到是哪个模块的锅
- 为什么?车上出问题很难复现,必须靠日志和监控
4.2 推荐执行结构
一个专用推理引擎的典型执行结构:
┌─────────────────────────────────────────────────────┐
│ Inference Engine │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ PreInit │ │ Runtime │ │ PostProc │ │
│ │ (初始化) │ │ (推理) │ │ (后处理) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 初始化阶段: │
│ - 加载模型权重 │
│ - 预分配所有中间张量内存 │
│ - 预热(Warmup)所有kernel │
│ - 注册回调和监控钩子 │
│ │
│ 推理阶段(每帧调用,循环执行): │
│ ┌──────────────────────────────────────┐ │
│ │ 1. 输入拷贝/预处理 │ │
│ │ 2. Backbone 推理 │ │
│ │ 3. View Transform 推理 │ │
│ │ 4. BEV Encoder 推理 │ │
│ │ 5. Temporal Fusion 推理 │ │
│ │ 6. Detection Head 推理 │ │
│ │ 7. PlanNN 推理 │ │
│ │ 8. 后处理 + 输出 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 监控与诊断: │
│ - 每个阶段的耗时统计 │
│ - 输出范围检查(NaN/Inf检测) │
│ - 超时告警 │
└─────────────────────────────────────────────────────┘
cpp
// 伪代码:专用推理引擎的C++接口设计
class BEVInferenceEngine {
public:
// 初始化:加载模型、分配内存、预热
bool init(const EngineConfig& config);
// 推理:每帧调用一次
bool infer(const InputData& input, OutputData& output);
// 销毁:释放资源
void destroy();
// 获取性能统计
PerformanceStats get_stats();
private:
// 各子模块
std::unique_ptr<BackboneModule> backbone_;
std::unique_ptr<ViewTransformModule> view_transform_;
std::unique_ptr<BEVEncoderModule> bev_encoder_;
std::unique_ptr<TemporalFusionModule> temporal_fusion_;
std::unique_ptr<DetectionHeadModule> det_head_;
std::unique_ptr<PlanNNModule> plannn_;
// 预分配的中间张量(推理过程中复用)
Tensor img_features_; // Backbone输出
Tensor bev_features_; // View Transform输出
Tensor bev_encoded_; // BEV Encoder输出
Tensor bev_fused_; // Temporal Fusion输出
// 性能统计
PerformanceStats stats_;
// 超时监控
std::chrono::microseconds deadline_;
};
五、模型结构优化与算子定制
光有好的架构还不够,模型本身的计算效率才是根本。专用推理引擎的一大优势就是可以和算法团队深度配合,从模型结构层面做优化。
5.1 结构优化方向对比
| 优化方向 | 原理 | 收益 | 代价 | 难度 |
|---|---|---|---|---|
| 通道剪枝 | 去掉不重要的卷积通道 | 减少计算量和参数量 | 精度可能下降 | 中 |
| 深度可分离卷积 | 把标准卷积拆成Depthwise + Pointwise | 大幅减少计算量 | 精度可能下降,需要重新训练 | 高(需重训) |
| 替换激活函数 | 用ReLU/SiLU等硬件友好的激活 | 减少算子开销 | 视情况 | 低 |
| 算子融合 | 把多个小算子合并成一个大算子 | 减少访存和启动开销 | 需定制kernel | 中高 |
| 降低精度 | FP32→FP16→INT8→INT4 | 显存和算力减半或更多 | 精度损失 | 中 |
| 注意力优化 | 局部注意力、降采样注意力 | 减少O(n²)复杂度 | 精度可能下降 | 高(需重训) |
| 输入分辨率调整 | 降低输入图像分辨率 | 计算量平方级下降 | 检测精度下降 | 低 |
关键原则 :模型结构优化和算法团队一起做,不要自己闷头搞。 推理工程师说"这个算子不好优化",算法工程师说"那我们换个结构",往往能取得1+1>2的效果。
5.2 算子定制流程
对于那些通用优化手段搞不定的热点算子,就需要上"大招"------自定义算子(CUDA Kernel / TensorRT Plugin / 芯片厂商的自定义算子接口)。
算子定制的标准流程:
1. Profile定位热点
│
▼
2. 算子分类(计算密集型 / 访存密集型 / 控制密集型)
│
▼
3. 融合分析(这个算子能不能和前后的算子合并?)
│
▼
4. 自定义Kernel开发
│ ├─ 算法设计(分块、向量化、共享内存使用)
│ ├─ 实现与调试
│ └─ 性能调优
│
▼
5. 正确性验证
│ ├─ 数值精度对比(和参考实现的误差范围)
│ └─ 边界case测试
│
▼
6. 回归测试
└─ 确保不影响其他模块,整体精度达标
我们以View Transform中的一个典型操作为例,看看算子融合怎么做:
python
# 优化前:View Transform中的典型操作链
# 多个小算子串联,每个都有独立的kernel启动和访存开销
def view_transform_ref(image_feats, points, depths):
"""
参考实现:多个小算子串联
"""
# Step 1: 投影点采样(grid_sample)
sampled = grid_sample(image_feats, points) # 1次kernel launch
# Step 2: 乘以深度权重
weighted = sampled * depths # 1次kernel launch
# Step 3: 沿着视角维度累加
bev_feat = sum_over_views(weighted) # 1次kernel launch
# Step 4: 归一化
bev_feat = normalize(bev_feat) # 1次kernel launch
return bev_feat
# 总共4次kernel launch,4次访存
c++
// 优化后:融合成一个CUDA Kernel
// 所有操作在一个kernel里完成,中间结果存在寄存器或共享内存里
__global__ void fused_view_transform_kernel(
const float* image_feats, // 图像特征
const float* points, // 投影点坐标
const float* depths, // 深度权重
float* bev_feat, // 输出BEV特征
int num_views, int C, int H, int W, int bev_h, int bev_w)
{
// 每个线程处理一个BEV网格点
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= bev_h * bev_w) return;
float sum = 0.0f;
// 遍历所有视角,累加特征
for (int v = 0; v < num_views; v++) {
// 计算投影坐标(内联计算,不需要额外内存)
float2 pt = get_projection_point(points, v, idx);
// 双线性采样(内联实现,不需要调用grid_sample)
float feat[C];
bilinear_sample(image_feats + v * C * H * W, pt.x, pt.y, C, H, W, feat);
// 乘以深度权重(内联,不需要额外内存)
float depth_w = depths[v * bev_h * bev_w + idx];
for (int c = 0; c < C; c++) {
sum += feat[c] * depth_w;
}
}
// 归一化并写出(最后只写一次)
bev_feat[idx] = sum / num_views;
}
// 总共1次kernel launch,1次读+1次写
融合的收益来自哪里?
- 减少Kernel Launch开销:从4次变成1次
- 减少访存:中间结果存在寄存器/共享内存里,不用反复读写显存
- 减少内存占用:不需要存中间张量
经验值:对于访存密集型的算子链,融合通常能带来2~5倍的性能提升。
六、推理流水线并行优化
BEV感知是一个多阶段的流水线,除了优化每个阶段的速度,流水线本身的组织方式也很重要。
6.1 Sequential vs Pipelined
顺序执行(Sequential):
时间 →
CPU: [预处理]
GPU: [Backbone] [ViewTransform] [BEVEncoder] [检测头] [PlanNN]
下一帧: [预处理] ...
- 简单直接,一帧走完再走下一帧
- GPU利用率不高,CPU和GPU串行
流水线执行(Pipelined):
时间 →
帧0 CPU: [预处理]
帧0 GPU: [Backbone] [ViewTransform] [BEVEncoder] [检测头] [PlanNN]
帧1 CPU: [预处理]
帧1 GPU: [Backbone] [ViewTransform] [BEVEncoder] [检测头] [PlanNN]
帧2 CPU: [预处理]
帧2 GPU: [Backbone] ...
- CPU和GPU并行:GPU在算第N帧时,CPU在预处理第N+1帧
- 提高了整体吞吐
- 但端到端延迟增加了一帧(因为有流水线填充)
6.2 关键结论
- 对于单帧延迟要求极高的场景(如自动驾驶),流水线的级数不能太多------每多一级就多一帧延迟
- CPU/GPU并行是必须做的------收益大,实现简单,几乎没有副作用
- 多帧流水线要谨慎------虽然吞吐提升了,但延迟也增加了。自动驾驶更看重延迟而非吞吐
python
# 伪代码:CPU/GPU并行的推理循环
def inference_loop(engine, camera_stream):
"""
CPU/GPU 流水线:
- GPU在跑第N帧推理时,CPU在预处理第N+1帧
"""
# 预分配双缓冲
buffer_0 = PreprocessBuffer()
buffer_1 = PreprocessBuffer()
current_buffer = 0
# GPU事件,用于同步
gpu_event = cudaEventCreate()
for frame_idx, frame_data in enumerate(camera_stream):
# 1. CPU: 预处理当前帧(同时GPU可能在跑上一帧)
if current_buffer == 0:
preprocess(frame_data, buffer_0)
else:
preprocess(frame_data, buffer_1)
# 2. 等待上一帧的GPU推理完成
if frame_idx > 0:
cudaEventSynchronize(gpu_event)
# 上一帧的结果已经就绪,可以做后处理
post_process(output_buffer)
# 3. GPU: 启动当前帧的推理(异步)
input_buffer = buffer_0 if current_buffer == 0 else buffer_1
engine.infer_async(input_buffer, output_buffer)
cudaEventRecord(gpu_event)
# 4. 切换缓冲区
current_buffer = 1 - current_buffer
6.3 工程手段小结
实现BEV推理流水线优化的常用工程手段:
| 手段 | 作用 | 实现复杂度 | 收益 |
|---|---|---|---|
| CPU/GPU 并行 | 预处理和推理并行 | 低 | 中 |
| 双缓冲/多缓冲 | 避免数据拷贝等待 | 低 | 中 |
| CUDA Stream | 多流并行,隐藏数据传输 | 中 | 中高 |
| CUDA Graph | 捕获并复用kernel launch序列,减少启动开销 | 中 | 中 |
| 多线程流水线 | 多阶段流水线并行 | 高 | 高(但增加延迟) |
七、算法-硬件协同:面向硬件设计模型
最后讲一个"降维打击"级别的优化思路:算法-硬件协同设计(Algorithm-Hardware Co-design)。
什么意思呢?就是算法团队在设计模型结构的时候,就考虑到目标硬件的特性,尽量用硬件"喜欢"的操作,避开硬件"不喜欢"的操作。这样做出来的模型,天然就跑得快。
7.1 硬件友好 vs 谨慎使用的操作
硬件友好的操作(放心用):
- 大矩阵乘法(GEMM):GPU和NPU都是为这个设计的
- 标准卷积(Conv2d):有专门的硬件加速单元(Tensor Core / DSP)
- 逐元素操作(Element-wise):加、减、乘、ReLU等,简单高效
- 池化(Pooling):Max Pooling、Average Pooling
- 规则的内存访问:连续地址、对齐访问
谨慎使用的操作(尽量少用):
- 动态形状:运行时才知道大小,很多硬件不支持
- 稀疏计算:硬件利用率低(除非有专门的稀疏加速单元)
- 非规则访存:gather/scatter、索引操作,cache命中率低
- 控制流:if-else、循环,破坏并行性
- 特殊激活函数:SiLU、GELU虽然好,但ReLU在某些硬件上更快
- 大维度的Transpose/Reshape:数据搬运开销大
7.2 面向车载硬件的设计规则
针对车载推理芯片(Orin、Thor、昇腾、地平线等),有一些通用的设计经验:
1. 通道数凑整
- 卷积的通道数尽量是8/16/32的倍数(取决于硬件的向量宽度)
- 比如不要用33通道,尽量用32或48
- 为什么?硬件的向量处理单元是按固定宽度工作的,凑不满就浪费
2. 输入尺寸对齐
- 特征图的宽高尽量对齐到硬件要求的边界(如16像素对齐)
- 必要时可以加padding
- 为什么?内存对齐后访问效率更高
3. 避免形状动态变化
- 尽量让所有张量的形状在编译期就确定
- 不要在推理过程中动态reshape、动态split
- 为什么?动态形状需要运行时重新分配内存、重新调度,开销大且不确定
4. 减少数据搬运
- 模型结构上尽量让数据"流"起来,减少大张量的复制
- View Transform这类操作尽量融合,减少中间结果的存储
- 为什么?内存带宽往往是瓶颈,不是算力
5. 精度选择要务实
- 不要盲目追求FP32,FP16甚至INT8在大多数感知任务上都够用
- 用量化感知训练(QAT)来弥补量化带来的精度损失
- 为什么?低精度=更快的速度+更少的显存+更低的功耗
python
# 伪代码:硬件友好的模型设计检查清单
def hardware_friendly_check(model, target_hw="orin"):
"""检查模型是否对目标硬件友好"""
issues = []
for name, module in model.named_modules():
# 检查通道数是否对齐
if isinstance(module, Conv2d):
if module.in_channels % 16 != 0:
issues.append(f"{name}: in_channels ({module.in_channels}) 不是16的倍数")
if module.out_channels % 16 != 0:
issues.append(f"{name}: out_channels ({module.out_channels}) 不是16的倍数")
# 检查是否有不推荐的操作
if isinstance(module, Gather) or isinstance(module, Scatter):
issues.append(f"{name}: 使用了gather/scatter,访存可能低效")
# 检查是否有动态形状
if has_dynamic_shape(module):
issues.append(f"{name}: 存在动态形状,可能影响性能和确定性")
return issues
八、小结与思考
专用推理引擎的开发,和通用LLM Serving是两种完全不同的思路。我们来回顾一下核心要点:
-
理解差异:专用推理(如BEV)和通用LLM推理在输入输出、延迟要求、失败成本、硬件环境、优化目标上都有本质区别。不要拿着LLM的锤子去敲所有钉子。
-
先Profile再优化:找到计算热点,把精力花在占比最大的地方。BEV中Backbone和View Transform通常是最大的两个热点。
-
架构设计原则:静态化、预分配、零拷贝、确定性、可回退、可观测------这六条是专用推理引擎的立身之本。
-
算子定制是大招:对于热点算子,融合是性价比最高的优化手段。把多个小算子合并成一个,减少访存和启动开销。
-
流水线优化:CPU/GPU并行必做,几乎零成本。多帧流水线要权衡吞吐和延迟,自动驾驶场景优先保证延迟。
-
算法-硬件协同是终极武器:从模型结构层面就面向硬件设计,这是"降维打击"级别的优化。推理工程师要和算法工程师紧密配合。
最后想说:通用框架解决的是80%的场景,专用引擎解决的是剩下20%的硬骨头。 不是每个业务都需要自己做推理引擎,但如果你面对的是极致的性能要求、特殊的硬件环境、或者极高的安全要求,那么掌握专用推理引擎的开发方法,就成了核心竞争力。