实时视觉:边缘部署 本节摘要:边缘推理的纪律,是让一个 90% 准确率的模型在 2 GB 内存的设备上跑 30 fps——每一个百分点的准确率都要拿毫秒级延迟来换。本节先立测量纪律(延迟、峰值内存、功耗三个预算,加预热、同步、固定输入三条铁律),再走三个旋钮:选更小的架构、把 FP32 量化成 INT8、换推理运行时(ONNX Runtime、TensorRT、Core ML、TFLite)。读完本节,你能为手机、Jetson、工业相机、浏览器挑出 MobileNetV3 / EfficientNet-Lite / ConvNeXt-Tiny / MobileViT,并验证每一档省了多少、掉了多少精度。 对应原课程:Phase 4 · Lesson 15 · (原英文 )。
本节摘要:边缘推理的纪律,是让一个 90% 准确率的模型在 2 GB 内存的设备上跑 30 fps——每一个百分点的准确率都要拿毫秒级延迟来换。本节先立测量纪律(延迟、峰值内存、功耗三个预算,加预热、同步、固定输入三条铁律),再走三个旋钮:选更小的架构、把 FP32 量化成 INT8、换推理运行时(ONNX Runtime、TensorRT、Core ML、TFLite)。读完本节,你能为手机、Jetson、工业相机、浏览器挑出 MobileNetV3 / EfficientNet-Lite / ConvNeXt-Tiny / MobileViT,并验证每一档省了多少、掉了多少精度。
对应原课程:Phase 4 · Lesson 15 ·
real-time-edge(原英文phases/04-computer-vision/15-real-time-edge/docs/en.md)。
阅读完本节,你应当能够:
训练时的视觉模型是一头浮点怪兽:一亿参数、每次前向 10 GFLOPs、2 GB 显存。手机、车机、工业相机、无人机都装不下。上线一套视觉系统,意味着把同样的预测塞进一个百倍小的预算里。
三个旋钮干了绝大部分活:模型选择(同配方下更小的架构)、量化(INT8 替 FP32)、推理运行时(ONNX Runtime、TensorRT、Core ML、TFLite)。把它们摆对,是「工作站上跑得动的 demo」与「30 美元摄像头模组上能出货的产品」之间的分野。
本节先把测量纪律立起来(测不了就优化不了),再走这三个旋钮。目标不是学遍每个边缘运行时,而是知道有哪些杠杆、怎么验证每个杠杆真的有效。
一张 (模型, 延迟, 内存, 精度) 表就是做边缘决策的依据。每个格子都在目标设备上测,不是工作站。
每份边缘性能剖析都该遵守三条:
torch.cuda.synchronize()。否则测的是 kernel 派发,不是 kernel 执行。FLOPs(每推理浮点运算数)是便宜的、与设备无关的延迟代理。对架构比较有用,作绝对时钟则有误导。FLOPs 多 10% 的模型可能实测快 2 倍,因为它用的是硬件友好的算子(深度卷积编译得好,7×7 大卷积编译不好)。
规矩:架构搜索用 FLOPs,部署决策用设备实测延迟。
把 FP32 权重和激活换成 INT8。模型体积缩 4 倍,内存带宽缩 4 倍,在有 INT8 kernel 的硬件上(每个现代手机 SoC、每个带 Tensor Core 的 NVIDIA GPU)算力提升 24 倍。训练后静态量化在视觉任务上精度损失通常 0.11 个百分点。
类型:
视觉任务上,训练后静态量化用 5% 的功夫拿到 95% 的收益。只有 PTQ 精度损失不可接受时才用 QAT。
torch.compile 和 ONNX 导出取代。.mlmodel 或 .mlpackage。.tflite。.xml + .bin。实践:PyTorch → ONNX → 为目标选运行时。ONNX 是通用语。
| 预算 | 模型 | 为何 |
|---|---|---|
| < 300 万参数 | MobileNetV3-Small | 处处编译得好,好基线 |
| 300~1000 万 | EfficientNet-Lite-B0 | TFLite 上每参数精度最佳 |
| 1000~2000 万 | ConvNeXt-Tiny | 每参数精度最佳,CPU 友好 |
| 2000~3000 万 | MobileViT-S 或 EfficientViT | 带 ImageNet 精度的 Transformer |
| 3000~8000 万 | Swin-V2-Tiny | 仅当栈支持窗口注意力 |
除非有特定理由,这些全部量化到 INT8。
import time import torch def measure_latency(model, input_shape, device="cpu", warmup=10, iters=50): model = model.to(device).eval() x = torch.randn(input_shape, device=device) with torch.no_grad(): for _ in range(warmup): model(x) if device == "cuda": torch.cuda.synchronize() times = [] for _ in range(iters): if device == "cuda": torch.cuda.synchronize() t0 = time.perf_counter() model(x) if device == "cuda": torch.cuda.synchronize() times.append((time.perf_counter() - t0) * 1000) times.sort() return { "p50_ms": times[len(times) // 2], "p95_ms": times[int(len(times) * 0.95)], "p99_ms": times[int(len(times) * 0.99)], "mean_ms": sum(times) / len(times), }
预热、同步、用 time.perf_counter()。报百分位,不只均值。
def parameter_count(model): return sum(p.numel() for p in model.parameters()) def flops_estimate(model, input_shape): """ 仅卷积/线性模型的粗略 FLOP 计数。生产用 fvcore 或 ptflops。 """ total = 0 def conv_hook(m, inp, out): nonlocal total c_out, c_in, kh, kw = m.weight.shape h, w = out.shape[-2:] total += 2 * c_in * c_out * kh * kw * h * w def linear_hook(m, inp, out): nonlocal total total += 2 * m.in_features * m.out_features hooks = [] for m in model.modules(): if isinstance(m, torch.nn.Conv2d): hooks.append(m.register_forward_hook(conv_hook)) elif isinstance(m, torch.nn.Linear): hooks.append(m.register_forward_hook(linear_hook)) model.eval() with torch.no_grad(): model(torch.randn(input_shape)) for h in hooks: h.remove() return total
真实项目用 fvcore.nn.FlopCountAnalysis 或 ptflops,它们正确处理每种模块。
def quantise_ptq(model, calibration_loader, backend="x86"): import torch.ao.quantization as tq model = model.eval().cpu() model.qconfig = tq.get_default_qconfig(backend) tq.prepare(model, inplace=True) with torch.no_grad(): for x, _ in calibration_loader: model(x) tq.convert(model, inplace=True) return model
三步:配置、prepare(插观察器)、用真实数据校准、convert(融合+量化)。要求模型已融合(Conv → BN → ReLU → ConvBnReLU),由 torch.ao.quantization.fuse_modules 处理。
def export_onnx(model, sample_input, path="model.onnx"): model = model.eval() torch.onnx.export( model, sample_input, path, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=17, ) return path
opset_version=17 是 2026 年的安全默认。dynamic_axes 让 ONNX 模型能跑任意批大小。
import torch.nn as nn from torchvision.models import mobilenet_v3_small def compare_regimes(): model = mobilenet_v3_small(weights=None, num_classes=10) params = parameter_count(model) flops = flops_estimate(model, (1, 3, 224, 224)) lat_fp32 = measure_latency(model, (1, 3, 224, 224), device="cpu") print(f"FP32 MobileNetV3-Small: {params:,} 参数 {flops/1e9:.2f} GFLOPs " f"p50={lat_fp32['p50_ms']:.2f}ms p95={lat_fp32['p95_ms']:.2f}ms")
对 resnet50、efficientnet_v2_s、convnext_tiny 跑同一函数,就拿到部署决策要的那张对比表。
生产栈收敛到三条路之一:
测量上,torch-tb-profiler、nvprof/nsys、macOS 的 Instruments 给逐层拆解。benchmark_app(OpenVINO)和 trtexec(TensorRT)给独立 CLI 数字。
本节产出两个可复用文件(位于原课程 outputs/):
prompt-edge-deployment-planner.md:一个提示词——给定目标设备和延迟 SLA,挑主干、量化策略和运行时。skill-latency-profiler.md:一个技能——写出完整的延迟基准脚本,含预热、同步、百分位、内存追踪。resnet18、mobilenet_v3_small、efficientnet_v2_s、convnext_tiny 在 224×224 的 p50 延迟。报告表格,指出哪个架构每毫秒精度最佳。mobilenet_v3_small 做训练后静态量化。报告 FP32 vs INT8 延迟,以及在 CIFAR-10 或类似数据留出子集上的精度损失。convnext_tiny 导出 ONNX,用 onnxruntime 的 CPUExecutionProvider 跑,对比 PyTorch eager 基线的延迟。找出 ONNX Runtime 更快的第一层,解释为什么。下一节是本章毕业项目——把检测、跟踪、分类串成一条端到端实时视觉流水线,综合前面所有技能。