边缘 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
  模型很少更新 → 端侧打包

总结

  1. ONNX Runtime 是跨平台首选:一次转换,CPU/GPU/NPU/浏览器全平台运行。
  2. Apple 平台优先 Core ML:ANE 加速对电池和散热友好,是 iOS 部署的最优选择。
  3. WebGPU 正在改变浏览器推理:Transformers.js + WebGPU 让浏览器端运行中小模型成为现实。
  4. 端侧 LLM 已经可用:3B 参数以下的量化模型在手机上已经有实用的推理速度。
  5. 优化是组合拳:知识蒸馏 + 量化 + 图优化的组合可以实现 10-100x 的部署效率提升。

Maurice | maurice_wen@proton.me