边缘 AI 部署:从云端到终端
原创
灵阙教研团队
S 精选 进阶 |
约 7 分钟阅读
更新于 2026-02-28 AI 导读
边缘 AI 部署:从云端到终端 ONNX Runtime、TensorRT、Core ML、WebGPU 运行时对比,模型优化压缩技术与端侧推理实战 引言 云端 AI 推理面临三大制约:网络延迟(用户体验)、带宽成本(数据传输)和隐私合规(数据出境)。边缘 AI 将推理计算推到离用户最近的位置——手机、浏览器、IoT 设备甚至芯片内部——从根本上消除了这些制约。 本文覆盖边缘 AI...
边缘 AI 部署:从云端到终端
ONNX Runtime、TensorRT、Core ML、WebGPU 运行时对比,模型优化压缩技术与端侧推理实战
引言
云端 AI 推理面临三大制约:网络延迟(用户体验)、带宽成本(数据传输)和隐私合规(数据出境)。边缘 AI 将推理计算推到离用户最近的位置——手机、浏览器、IoT 设备甚至芯片内部——从根本上消除了这些制约。
本文覆盖边缘 AI 部署的全链路:从模型优化到多平台运行时选型,从浏览器推理到手机端部署。
运行时全景
主流推理运行时对比
| 运行时 | 平台 | 硬件 | 语言 | 模型格式 | 典型延迟 | 适用场景 |
|---|---|---|---|---|---|---|
| ONNX Runtime | 全平台 | CPU/GPU/NPU | C++/Python/JS | ONNX | 基线 | 跨平台通用 |
| TensorRT | Linux/Windows | NVIDIA GPU | C++/Python | TRT Engine | 基线 0.3x | GPU 服务器/边缘 |
| Core ML | Apple 全平台 | CPU/GPU/ANE | Swift/ObjC | mlmodel/mlpackage | 基线 0.5x | iPhone/iPad/Mac |
| TFLite | Android/Linux | CPU/GPU/NPU | Java/C++ | .tflite | 基线 0.8x | Android 设备 |
| WebGPU/WASM | 浏览器 | GPU/CPU | JS/WASM | ONNX/custom | 基线 2-5x | 浏览器推理 |
| llama.cpp | 全平台 | CPU/GPU | C++ | GGUF | - | LLM 端侧推理 |
| MLC-LLM | 全平台 | CPU/GPU/NPU | Python/C++ | MLC 格式 | - | LLM 移动端 |
ONNX Runtime 实战
模型转换与优化
# Step 1: Export PyTorch model to ONNX
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Create dummy input
dummy_input = tokenizer(
"This is a sample sentence",
return_tensors="pt",
padding="max_length",
max_length=128,
truncation=True,
)
# Export to ONNX
torch.onnx.export(
model,
(dummy_input["input_ids"], dummy_input["attention_mask"]),
"sentiment.onnx",
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch_size", 1: "sequence"},
"attention_mask": {0: "batch_size", 1: "sequence"},
"logits": {0: "batch_size"},
},
opset_version=17,
)
# Step 2: Optimize ONNX model
from onnxruntime.transformers import optimizer
optimized_model = optimizer.optimize_model(
"sentiment.onnx",
model_type="bert",
num_heads=12,
hidden_size=768,
optimization_options=None,
)
optimized_model.convert_float_to_float16()
optimized_model.save_model_to_file("sentiment_optimized.onnx")
ONNX Runtime 推理
# Step 3: Run optimized inference
import onnxruntime as ort
import numpy as np
# Configure session
session_options = ort.SessionOptions()
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session_options.intra_op_num_threads = 4
session_options.inter_op_num_threads = 1
# Choose execution provider
providers = [
("CUDAExecutionProvider", {
"device_id": 0,
"arena_extend_strategy": "kNextPowerOfTwo",
}),
"CPUExecutionProvider",
]
session = ort.InferenceSession(
"sentiment_optimized.onnx",
session_options,
providers=providers,
)
# Run inference
inputs = tokenizer("Great movie!", return_tensors="np", max_length=128, padding="max_length")
logits = session.run(
["logits"],
{
"input_ids": inputs["input_ids"],
"attention_mask": inputs["attention_mask"],
},
)[0]
prediction = np.argmax(logits, axis=1)
print(f"Sentiment: {'positive' if prediction[0] == 1 else 'negative'}")
Core ML 部署(Apple 平台)
模型转换
# Convert to Core ML format
import coremltools as ct
# From ONNX
mlmodel = ct.converters.onnx.convert(
model="sentiment_optimized.onnx",
minimum_deployment_target=ct.target.iOS17,
)
# Optimize for Apple Neural Engine (ANE)
mlmodel = ct.convert(
model,
convert_to="mlprogram",
compute_units=ct.ComputeUnit.ALL, # CPU + GPU + ANE
compute_precision=ct.precision.FLOAT16,
)
# Add metadata
mlmodel.author = "AI Team"
mlmodel.short_description = "Sentiment classification model"
mlmodel.input_description["input_ids"] = "Tokenized input IDs"
mlmodel.save("SentimentClassifier.mlpackage")
Swift 集成
// iOS/macOS inference with Core ML
import CoreML
import NaturalLanguage
class SentimentPredictor {
private let model: SentimentClassifier
private let tokenizer: BertTokenizer
init() throws {
let config = MLModelConfiguration()
config.computeUnits = .all // Use ANE when available
self.model = try SentimentClassifier(configuration: config)
self.tokenizer = BertTokenizer()
}
func predict(text: String) throws -> (label: String, confidence: Double) {
// Tokenize input
let tokens = tokenizer.encode(text, maxLength: 128)
// Create MLMultiArray inputs
let inputIds = try MLMultiArray(shape: [1, 128], dataType: .int32)
let attentionMask = try MLMultiArray(shape: [1, 128], dataType: .int32)
for (i, token) in tokens.inputIds.enumerated() {
inputIds[i] = NSNumber(value: token)
attentionMask[i] = NSNumber(value: tokens.attentionMask[i])
}
// Run inference
let input = SentimentClassifierInput(
input_ids: inputIds,
attention_mask: attentionMask
)
let output = try model.prediction(input: input)
// Parse logits
let logits = output.logits
let positive = logits[1].doubleValue
let negative = logits[0].doubleValue
let isPositive = positive > negative
return (
label: isPositive ? "positive" : "negative",
confidence: isPositive ? softmax(positive, negative) : softmax(negative, positive)
)
}
private func softmax(_ a: Double, _ b: Double) -> Double {
let expA = exp(a)
let expB = exp(b)
return expA / (expA + expB)
}
}
WebGPU 浏览器推理
WebGPU + ONNX Runtime Web
// Browser-side inference with ONNX Runtime Web
import * as ort from "onnxruntime-web";
// Configure WebGPU backend
ort.env.wasm.numThreads = 4;
async function initModel(): Promise<ort.InferenceSession> {
const session = await ort.InferenceSession.create(
"/models/sentiment.onnx",
{
executionProviders: ["webgpu", "wasm"], // Fallback chain
graphOptimizationLevel: "all",
}
);
return session;
}
async function predict(
session: ort.InferenceSession,
text: string,
): Promise<{ label: string; score: number }> {
// Tokenize (using a JS tokenizer like @xenova/transformers)
const { AutoTokenizer } = await import("@xenova/transformers");
const tokenizer = await AutoTokenizer.from_pretrained(
"distilbert-base-uncased-finetuned-sst-2-english"
);
const encoded = tokenizer(text, {
padding: "max_length",
max_length: 128,
truncation: true,
return_tensors: "js",
});
// Create tensors
const inputIds = new ort.Tensor("int64", encoded.input_ids.data, [1, 128]);
const attentionMask = new ort.Tensor("int64", encoded.attention_mask.data, [1, 128]);
// Run inference
const results = await session.run({
input_ids: inputIds,
attention_mask: attentionMask,
});
const logits = results.logits.data as Float32Array;
const isPositive = logits[1] > logits[0];
return {
label: isPositive ? "positive" : "negative",
score: Math.max(logits[0], logits[1]),
};
}
Transformers.js(浏览器端完整方案)
// Using Hugging Face Transformers.js
import { pipeline } from "@xenova/transformers";
// Load model (automatically downloads and caches)
const classifier = await pipeline(
"sentiment-analysis",
"Xenova/distilbert-base-uncased-finetuned-sst-2-english",
{ device: "webgpu" }, // Use WebGPU if available
);
// Run inference
const result = await classifier("I love this product!");
console.log(result);
// [{ label: "POSITIVE", score: 0.9998 }]
// Batch inference
const results = await classifier([
"Great experience!",
"Terrible service.",
"It was okay.",
]);
模型优化技术
优化流水线
原始模型 (PyTorch FP32)
│
├─ 知识蒸馏 (Teacher → Student)
│ 原始: BERT-Large (340M) → 蒸馏: DistilBERT (66M)
│ 精度损失: ~1-3%
│
├─ 剪枝 (Pruning)
│ 移除不重要的权重/通道
│ 非结构化: 50-90% 稀疏度
│ 结构化: 30-50% 通道剪枝
│
├─ 量化 (Quantization)
│ FP32 → INT8: 4x 压缩, ~1% 精度损失
│ FP32 → INT4: 8x 压缩, ~3% 精度损失
│
└─ 图优化 (Graph Optimization)
算子融合、常量折叠、内存优化
通常 10-30% 额外加速
各平台优化对照
| 优化 | ONNX Runtime | TensorRT | Core ML | TFLite |
|---|---|---|---|---|
| INT8 量化 | 支持 | 支持(最优) | 支持 | 支持 |
| FP16 | 支持 | 支持 | 支持(默认) | 支持 |
| 算子融合 | 自动 | 自动(最优) | 自动 | 有限 |
| 动态 shape | 支持 | 部分支持 | 支持 | 有限 |
| 模型加密 | 不支持 | 不支持 | 支持 | 不支持 |
LLM 端侧部署
llama.cpp 移动端
# Build for iOS
cmake -B build-ios \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DLLAMA_METAL=ON \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-ios --config Release
# Run quantized model on iPhone
# Model: Llama-3.2-3B-Q4_K_M.gguf (~1.8GB)
# Performance: ~15 tokens/sec on iPhone 15 Pro (Metal)
端侧 LLM 可行性
| 设备 | 可用内存 | 推荐模型 | 量化 | 速度 |
|---|---|---|---|---|
| iPhone 15 Pro | 6GB | Llama 3.2 3B | Q4_K_M | ~15 tok/s |
| iPad Pro M4 | 16GB | Llama 3.1 8B | Q4_K_M | ~25 tok/s |
| MacBook Pro M3 | 36GB | Llama 3.1 70B | Q4_K_M | ~8 tok/s |
| Pixel 8 Pro | 12GB | Gemma 2 2B | Q4 | ~10 tok/s |
| RTX 4090 Laptop | 16GB VRAM | Llama 3.1 8B | FP16 | ~80 tok/s |
部署决策框架
模型部署位置决策:
问题 1: 模型大小?
< 50MB → 可以打包进 App
50-500MB → 首次启动下载 + 本地缓存
> 500MB → 云端推理或按需下载
问题 2: 延迟要求?
< 10ms → 必须端侧 (已加载模型)
10-100ms → 端侧或边缘节点
> 100ms → 云端可接受
问题 3: 隐私要求?
数据不能离开设备 → 端侧推理
数据可以到本地服务器 → 边缘节点
数据可以到云端 → 云端推理
问题 4: 更新频率?
模型每天更新 → 云端推理
模型每月更新 → 边缘节点 + OTA
模型很少更新 → 端侧打包
总结
- ONNX Runtime 是跨平台首选:一次转换,CPU/GPU/NPU/浏览器全平台运行。
- Apple 平台优先 Core ML:ANE 加速对电池和散热友好,是 iOS 部署的最优选择。
- WebGPU 正在改变浏览器推理:Transformers.js + WebGPU 让浏览器端运行中小模型成为现实。
- 端侧 LLM 已经可用:3B 参数以下的量化模型在手机上已经有实用的推理速度。
- 优化是组合拳:知识蒸馏 + 量化 + 图优化的组合可以实现 10-100x 的部署效率提升。
Maurice | maurice_wen@proton.me