摘要
- 邊緣AI是2026年最大增量市場:全球邊緣AI晶片出貨量預計超50億片,模型優化是落地的關鍵瓶頸
- 量化3級跳:FP16→INT8→INT4,INT8量化精度損失<1%,INT4需配合GPTQ/AWQ演算法
- 結構化剪枝2大路線:幅度剪枝簡單高效,蒸餾剪枝精度更好,組合使用可壓縮70%+參數
- 部署3大框架:ONNX Runtime通用性最強、TensorRT效能最優、TFLite行動端首選
- 本文提供從模型優化到邊緣部署的全鏈路實戰程式碼
目錄
邊緣AI:AI落地的最後十公里
邊緣AI市場現狀
| 設備類型 |
2026出貨量 |
典型算力 |
典型應用 |
| 智慧型手機 |
15億 |
30-60 TOPS |
語音助手、拍照增強 |
| 智慧攝影機 |
5億 |
2-8 TOPS |
人臉辨識、行為偵測 |
| 汽車(ADAS) |
1億 |
50-200 TOPS |
自動駕駛、DMS |
| IoT閘道器 |
3億 |
1-4 TOPS |
預測維護、異常偵測 |
| 工業控制器 |
5000萬 |
5-20 TOPS |
質檢、機器人控制 |
邊緣AI核心挑戰
| 挑戰 |
雲端 |
邊緣 |
差距 |
| 算力 |
1000+ TOPS |
2-60 TOPS |
50-500× |
| 記憶體 |
80-640GB |
2-16GB |
5-80× |
| 功耗 |
300W+ |
5-30W |
10-60× |
| 延遲 |
50-200ms(網路) |
1-10ms(本地) |
邊緣優勢 |
| 隱私 |
資料上雲 |
本地處理 |
邊緣優勢 |
邊緣AI晶片格局
| 晶片 |
廠商 |
算力 |
功耗 |
生態 |
| Jetson Orin NX |
NVIDIA |
100 TOPS |
25W |
CUDA+TensorRT |
| Hailo-8 |
Hailo |
26 TOPS |
2.5W |
專用編譯器 |
| Google Edge TPU |
Google |
4 TOPS |
2W |
TFLite |
| 瑞芯微RK3588 |
瑞芯微 |
6 TOPS |
10W |
RKNN |
| 華為昇騰310P |
華為 |
22 TOPS |
8W |
MindSpore |
| 地平線J5 |
地平線 |
128 TOPS |
15W |
天工開物 |
模型量化3級跳
量化原理與精度影響
┌──────────────────────────────────────────────────────────────┐
│ 量化3級跳 │
│ │
│ FP32 → FP16 (半精度) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 精度損失: <0.1% │ │
│ │ 體積縮減: 50% │ │
│ │ 速度提升: 1.5-2× │ │
│ │ 方法: 直接轉換,無需校準 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ FP16 → INT8 (8位整數量化) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 精度損失: 0.5-2% │ │
│ │ 體積縮減: 75% (vs FP32) │ │
│ │ 速度提升: 2-4× │ │
│ │ 方法: PTQ(訓練後量化) / QAT(量化感知訓練) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ INT8 → INT4 (4位整數量化) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 精度損失: 2-5% (需GPTQ/AWQ補償) │ │
│ │ 體積縮減: 87.5% (vs FP32) │ │
│ │ 速度提升: 3-6× │ │
│ │ 方法: GPTQ / AWQ / AQLM / HQQ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
INT8 PTQ量化實戰
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from neural_compressor import QuantizationAwareTraining, PostTrainingQuantConfig
def int8_ptq_quantize(model_path, output_path, calibration_data):
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_path)
config = PostTrainingQuantConfig(
approach="static",
tuning_criterion={
"max_trials": 100,
"objective": "accuracy",
},
quant_format="QDQ",
calibration_sampling_size=512,
)
def calibration_dataloader():
for text in calibration_data:
inputs = tokenizer(text, return_tensors="pt", max_length=512)
yield {"input_ids": inputs["input_ids"]}
from neural_compressor import quantize
q_model = quantize(
model,
config,
calibration_dataloader=calibration_dataloader,
)
q_model.save(output_path)
return q_model
def int8_gptq_quantize(model_path, output_path, calibration_data):
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoGPTQForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
)
quantize_config = BaseQuantizeConfig(
bits=8,
group_size=128,
desc_act=True,
damp_percent=0.01,
sym=True,
)
examples = []
for text in calibration_data:
tokenized = tokenizer(text, return_tensors="pt", max_length=2048)
examples.append(tokenized["input_ids"])
model.quantize(examples, quantize_config=quantize_config)
model.save_quantized(output_path)
return model
INT4量化實戰(GPTQ/AWQ)
def int4_gptq_quantize(model_path, output_path, calibration_data):
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoGPTQForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map="auto",
)
quantize_config = BaseQuantizeConfig(
bits=4,
group_size=128,
desc_act=True,
damp_percent=0.01,
sym=False,
)
examples = []
for text in calibration_data[:256]:
tokenized = tokenizer(text, return_tensors="pt", max_length=2048)
examples.append(tokenized["input_ids"])
model.quantize(examples, quantize_config=quantize_config)
model.save_quantized(output_path)
return model
def int4_awq_quantize(model_path, output_path, calibration_data):
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM",
}
model.quantize(
tokenizer,
quant_config=quant_config,
calib_data=calibration_data[:256],
)
model.save_quantized(output_path)
return model
量化效果對比
| 量化方案 |
7B模型體積 |
推理速度 |
精度損失 |
顯存需求 |
| FP32 |
28GB |
1× |
基線 |
32GB |
| FP16 |
14GB |
1.8× |
<0.1% |
16GB |
| INT8 PTQ |
7GB |
3.2× |
0.5-2% |
8GB |
| INT8 GPTQ |
7GB |
3.5× |
0.3-1% |
8GB |
| INT4 GPTQ |
3.5GB |
5.0× |
2-4% |
4GB |
| INT4 AWQ |
3.5GB |
5.5× |
1-3% |
4GB |
結構化剪枝實戰
剪枝方法對比
| 方法 |
粒度 |
精度損失 |
硬體友好 |
複雜度 |
| 非結構化剪枝 |
單權重 |
小 |
差(稀疏計算) |
低 |
| 結構化剪枝 |
通道/層 |
中 |
好(密集計算) |
中 |
| 蒸餾剪枝 |
通道+蒸餾 |
小 |
好 |
高 |
| 自動剪枝(NAS) |
搜尋空間 |
小 |
好 |
極高 |
幅度剪枝實戰
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM
class MagnitudePruner:
def __init__(self, model, sparsity_ratio=0.5):
self.model = model
self.sparsity_ratio = sparsity_ratio
self.masks = {}
def compute_masks(self):
for name, module in self.model.named_modules():
if isinstance(module, nn.Linear):
weight = module.weight.data.abs()
threshold = torch.quantile(
weight.flatten(), self.sparsity_ratio
)
self.masks[name] = (weight > threshold).float()
def apply_masks(self):
for name, module in self.model.named_modules():
if name in self.masks and isinstance(module, nn.Linear):
module.weight.data *= self.masks[name]
def structured_prune(self):
for name, module in self.model.named_modules():
if isinstance(module, nn.Linear):
weight = module.weight.data
row_norms = weight.norm(dim=1)
threshold = torch.quantile(
row_norms, self.sparsity_ratio
)
keep_mask = (row_norms > threshold).float()
module.weight.data *= keep_mask.unsqueeze(1)
if module.bias is not None:
module.bias.data *= keep_mask
def fine_tune(self, dataloader, epochs=3, lr=1e-5):
optimizer = torch.optim.AdamW(self.model.parameters(), lr=lr)
for epoch in range(epochs):
for batch in dataloader:
outputs = self.model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()
self.apply_masks()
return self.model
def prune_model(model_path, output_path, sparsity=0.5):
model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch.float16, device_map="auto"
)
pruner = MagnitudePruner(model, sparsity_ratio=sparsity)
pruner.structured_prune()
model.save_pretrained(output_path)
return model
剪枝+蒸餾組合
class PruneDistillPipeline:
def __init__(self, teacher_path, student_path, sparsity=0.5):
self.teacher = AutoModelForCausalLM.from_pretrained(
teacher_path, torch_dtype=torch.float16, device_map="auto"
)
self.student = AutoModelForCausalLM.from_pretrained(
student_path, torch_dtype=torch.float16, device_map="auto"
)
self.pruner = MagnitudePruner(self.student, sparsity)
def train(self, dataloader, epochs=5):
optimizer = torch.optim.AdamW(self.student.parameters(), lr=2e-5)
kl_loss = nn.KLDivLoss(reduction="batchmean")
for epoch in range(epochs):
for batch in dataloader:
with torch.no_grad():
teacher_out = self.teacher(**batch)
student_out = self.student(**batch)
hard_loss = student_out.loss
soft_loss = kl_loss(
F.log_softmax(student_out.logits / 2.0, dim=-1),
F.softmax(teacher_out.logits / 2.0, dim=-1),
) * 4.0
total_loss = 0.3 * hard_loss + 0.7 * soft_loss
total_loss.backward()
optimizer.step()
optimizer.zero_grad()
self.pruner.apply_masks()
return self.student
剪枝效果對比
| 剪枝方案 |
參數保留 |
精度損失 |
推理加速 |
體積縮減 |
| 幅度剪枝50% |
50% |
3-5% |
1.8× |
50% |
| 幅度剪枝70% |
30% |
8-12% |
2.5× |
70% |
| 蒸餾剪枝50% |
50% |
1-3% |
1.8× |
50% |
| 蒸餾剪枝70% |
30% |
4-6% |
2.5× |
70% |
| 剪枝+量化 |
30%+INT8 |
5-8% |
5× |
85% |
ONNX Runtime部署實戰
PyTorch→ONNX匯出
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
def export_to_onnx(model_path, output_path, opset=17):
model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch.float16
)
tokenizer = AutoTokenizer.from_pretrained(model_path)
model.eval()
dummy_input = tokenizer("Hello", return_tensors="pt")
input_ids = dummy_input["input_ids"]
attention_mask = dummy_input["attention_mask"]
torch.onnx.export(
model,
(input_ids, attention_mask),
output_path,
opset_version=opset,
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch", 1: "seq_len"},
"attention_mask": {0: "batch", 1: "seq_len"},
"logits": {0: "batch", 1: "seq_len"},
},
do_constant_folding=True,
)
print(f"Exported to {output_path}")
ONNX Runtime推理
import onnxruntime as ort
import numpy as np
class ONNXInferenceEngine:
def __init__(self, model_path, provider="CUDAExecutionProvider"):
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess_options.intra_op_num_threads = 4
sess_options.inter_op_num_threads = 4
self.session = ort.InferenceSession(
model_path,
sess_options=sess_options,
providers=[provider],
)
self.io_binding = self.session.io_binding()
def infer(self, input_ids, attention_mask):
input_ids_np = np.array(input_ids, dtype=np.int64)
attention_mask_np = np.array(attention_mask, dtype=np.int64)
outputs = self.session.run(
["logits"],
{
"input_ids": input_ids_np,
"attention_mask": attention_mask_np,
},
)
return outputs[0]
def infer_with_io_binding(self, input_ids, attention_mask):
input_ids_tensor = ort.OrtValue.ortvalue_from_numpy(
np.array(input_ids, dtype=np.int64), "cuda", 0
)
attention_mask_tensor = ort.OrtValue.ortvalue_from_numpy(
np.array(attention_mask, dtype=np.int64), "cuda", 0
)
self.io_binding.bind_ortvalue_input("input_ids", input_ids_tensor)
self.io_binding.bind_ortvalue_input("attention_mask", attention_mask_tensor)
self.io_binding.bind_output("logits", "cuda", 0)
self.session.run_with_iobinding(self.io_binding)
return self.io_binding.get_outputs()[0].numpy()
ONNX Runtime效能對比
| 配置 |
延遲(ms) |
吞吐(req/s) |
顯存 |
| PyTorch FP16 |
45 |
22 |
8GB |
| ORT FP16 |
32 |
31 |
6GB |
| ORT INT8 |
18 |
55 |
4GB |
| ORT INT8+IO Binding |
14 |
71 |
4GB |
| ORT INT8+Batch=8 |
8/batch |
125 |
6GB |
TensorRT優化實戰
ONNX→TensorRT轉換
import tensorrt as trt
def build_tensorrt_engine(
onnx_path,
engine_path,
fp16=True,
int8=False,
max_batch_size=8,
max_workspace=4 << 30,
):
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(
1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)
parser = trt.OnnxParser(network, logger)
with open(onnx_path, "rb") as f:
if not parser.parse(f.read()):
for i in range(parser.num_errors):
print(f"Parse error: {parser.get_error(i)}")
return None
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, max_workspace)
if fp16:
config.set_flag(trt.BuilderFlag.FP16)
if int8:
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = None
profile = builder.create_optimization_profile()
profile.set_shape(
"input_ids",
min=(1, 1),
opt=(1, 128),
max=(max_batch_size, 2048),
)
profile.set_shape(
"attention_mask",
min=(1, 1),
opt=(1, 128),
max=(max_batch_size, 2048),
)
config.add_optimization_profile(profile)
engine = builder.build_serialized_network(network, config)
with open(engine_path, "wb") as f:
f.write(engine)
print(f"TensorRT engine saved to {engine_path}")
return engine
class TensorRTInference:
def __init__(self, engine_path):
self.logger = trt.Logger(trt.Logger.WARNING)
self.runtime = trt.Runtime(self.logger)
with open(engine_path, "rb") as f:
self.engine = self.runtime.deserialize_cuda_engine(f.read())
self.context = self.engine.create_execution_context()
def infer(self, input_ids, attention_mask):
import pycuda.driver as cuda
import pycuda.autoinit
input_ids_np = np.array(input_ids, dtype=np.int32)
attention_mask_np = np.array(attention_mask, dtype=np.int32)
d_input_ids = cuda.mem_alloc(input_ids_np.nbytes)
d_attention_mask = cuda.mem_alloc(attention_mask_np.nbytes)
cuda.memcpy_htod(d_input_ids, input_ids_np)
cuda.memcpy_htod(d_attention_mask, attention_mask_np)
output_shape = self.engine.get_binding_shape(2)
d_output = cuda.mem_alloc(
trt.volume(output_shape) * np.dtype(np.float32).itemsize
)
bindings = [int(d_input_ids), int(d_attention_mask), int(d_output)]
self.context.execute_v2(bindings)
output = np.empty(output_shape, dtype=np.float32)
cuda.memcpy_dtoh(output, d_output)
return output
TensorRT效能對比
| 配置 |
延遲(ms) |
吞吐(req/s) |
加速比 |
| PyTorch FP16 |
45 |
22 |
1× |
| ORT INT8 |
18 |
55 |
2.5× |
| TRT FP16 |
15 |
66 |
3× |
| TRT INT8 |
8 |
125 |
5.6× |
| TRT INT8+Batch=8 |
3/batch |
333 |
15× |
邊緣設備推理策略
分層推理架構
┌──────────────────────────────────────────────────────────────┐
│ 邊緣-雲協同推理架構 │
│ │
│ 雲端(大模型) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 70B模型 → 複雜推理、長文本生成、多輪對話 │ │
│ │ 延遲: 200-500ms | 成本: 高 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↕ 網路 │
│ 邊緣閘道器(中等模型) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 7B量化模型 → 日常推理、簡單對話、意圖辨識 │ │
│ │ 延遲: 20-50ms | 成本: 中 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↕ 本地匯流排 │
│ 終端設備(小模型) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 剪枝+量化小模型 → 關鍵詞偵測、喚醒詞、簡單分類 │ │
│ │ 延遲: 1-5ms | 成本: 無 │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
邊緣部署程式碼(Jetson)
import jetson.inference
import jetson.utils
class EdgeInferencePipeline:
def __init__(self, model_type="classification", threshold=0.7):
self.model = jetson.inference.initialize(model_type)
self.threshold = threshold
self.fallback_enabled = True
def infer_local(self, image):
try:
results = self.model.Detect(image)
confident = [r for r in results if r.Confidence >= self.threshold]
if not confident and self.fallback_enabled:
return self.infer_cloud(image)
return confident
except Exception as e:
if self.fallback_enabled:
return self.infer_cloud(image)
raise
def infer_cloud(self, image):
import requests
_, compressed = cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85])
response = requests.post(
"https://api.example.com/infer",
files={"image": compressed.tobytes()},
timeout=5.0,
)
return response.json()
def benchmark(self, image, iterations=100):
import time
latencies = []
for _ in range(iterations):
start = time.perf_counter()
self.infer_local(image)
latencies.append((time.perf_counter() - start) * 1000)
return {
"mean_ms": sum(latencies) / len(latencies),
"p50_ms": sorted(latencies)[len(latencies) // 2],
"p99_ms": sorted(latencies)[int(len(latencies) * 0.99)],
}
邊緣設備效能實測
| 設備 |
模型 |
量化 |
延遲(ms) |
精度 |
| Jetson Orin NX |
BERT-base |
FP16 |
12 |
99.2% |
| Jetson Orin NX |
BERT-base |
INT8 |
6 |
98.8% |
| Jetson Orin NX |
ResNet50 |
FP16 |
8 |
99.5% |
| Jetson Orin NX |
ResNet50 |
INT8 |
4 |
99.1% |
| RK3588 |
MobileBERT |
INT8 |
25 |
97.5% |
| RK3588 |
MobileNetV3 |
INT8 |
8 |
98.2% |
| 樹莓派5 |
TinyBERT |
INT8 |
120 |
95.8% |
| 樹莓派5 |
MobileNetV2 |
INT8 |
45 |
97.0% |
總結與引流
關鍵要點回顧
- 量化:INT8 PTQ是性價比最優選擇,INT4需GPTQ/AWQ補償精度
- 剪枝:結構化剪枝+蒸餾組合可壓縮70%+參數,精度損失可控
- 部署:ONNX Runtime通用、TensorRT極致效能、TFLite行動端
- 邊緣策略:分層推理+雲端回退,兼顧延遲與精度
邊緣AI優化路線
| 階段 |
優化手段 |
預期效果 |
| 第1步 |
FP16匯出+ONNX Runtime |
2×加速 |
| 第2步 |
INT8量化 |
4×加速 |
| 第3步 |
結構化剪枝 |
體積-50% |
| 第4步 |
TensorRT優化 |
6×加速 |
| 第5步 |
邊緣-雲協同 |
靈活部署 |
需要壓縮AI生成的圖片?試試我們的圖片壓縮工具和影片壓縮,快速優化邊緣部署素材。
延伸閱讀