Edge Ai Hello World 251231
🎯 端侧 AI 的 Hello World - 最小可运行示例
目标:一个能完整跑通数据流的最简单模型,用于学习追踪
📦 这个"最简单的东西"是什么?
定义
一个最小可运行的端侧 AI 数据流:
固定图片输入
↓
简单 CNN 模型(2 层卷积)
↓
PyTorch 推理
↓
转换为 ONNX
↓
转换为 TensorRT
↓
GPU 推理
↓
输出分类结果
每一步都可以单独验证。
🚀 完整代码(可运行)
步骤1:创建最简单的模型
# simple_model.py - 最简单的 CNN 模型
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# 只有 2 层卷积 + 1 层全连接
self.features = nn.Sequential(
# 层1: 输入 3x64x64 → 输出 16x32x32
nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 64x64 → 32x32
# 层2: 输入 16x32x32 → 输出 32x16x16
nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 32x32 → 16x16
)
# 分类头
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(32 * 16 * 16, 128),
nn.ReLU(),
nn.Linear(128, num_classes)
)
def forward(self, x):
features = self.features(x)
output = self.classifier(features)
return output
# 创建模型
model = SimpleCNN(num_classes=10)
# 测试推理
dummy_input = torch.randn(1, 3, 64, 64)
output = model(dummy_input)
print(f"输入形状: {dummy_input.shape}")
print(f"输出形状: {output.shape}")
print(f"输出值(前5个): {output[0, :5]}")
# 保存模型
torch.save(model.state_dict(), "simple_cnn.pth")
print("✅ 模型已保存到 simple_cnn.pth")
运行:
python3 simple_model.py
输出:
输入形状: torch.Size([1, 3, 64, 64])
输出形状: torch.Size([1, 10])
输出值(前5个): tensor([ 0.1234, -0.5678, 0.9012, ...])
✅ 模型已保存到 simple_cnn.pth
步骤2:准备固定输入(用于验证)
# create_test_input.py - 创建固定的测试输入
import torch
import numpy as np
from PIL import Image
# 方法1:创建随机图片(简单)
def create_random_image():
# 创建 64x64 RGB 随机图片
img_array = np.random.randint(0, 255, (64, 64, 3), dtype=np.uint8)
img = Image.fromarray(img_array)
# 保存
img.save("test_image_64x64.jpg")
print("✅ 随机测试图片已保存")
# 转换为 Tensor
img_tensor = torch.from_numpy(img_array).float() / 255.0
img_tensor = img_tensor.permute(2, 0, 1).unsqueeze(0) # (1, 3, 64, 64)
# 保存 tensor
torch.save(img_tensor, "test_input.pt")
print("✅ 测试 Tensor 已保存到 test_input.pt")
return img_tensor
# 方法2:创建固定图片(可复现)
def create_fixed_image():
# 创建固定图案(渐变)
img_array = np.zeros((64, 64, 3), dtype=np.uint8)
for i in range(64):
for j in range(64):
img_array[i, j, 0] = int(i * 4) # R: 0-255 (行渐变)
img_array[i, j, 1] = int(j * 4) # G: 0-255 (列渐变)
img_array[i, j, 2] = 128 # B: 固定
img = Image.fromarray(img_array)
img.save("test_image_fixed.jpg")
print("✅ 固定测试图片已保存")
# 转换为 Tensor
img_tensor = torch.from_numpy(img_array).float() / 255.0
img_tensor = img_tensor.permute(2, 0, 1).unsqueeze(0)
# 保存 tensor
torch.save(img_tensor, "test_input_fixed.pt")
print("✅ 固定测试 Tensor 已保存")
return img_tensor
if __name__ == "__main__":
print("创建测试输入...")
tensor1 = create_random_image()
tensor2 = create_fixed_image()
print(f"\n随机 Tensor 形状: {tensor1.shape}")
print(f"固定 Tensor 形状: {tensor2.shape}")
print(f"固定 Tensor 范围: [{tensor2.min():.3f}, {tensor2.max():.3f}]")
运行:
python3 create_test_input.py
输出:
✅ 随机测试图片已保存
✅ 测试 Tensor 已保存到 test_input.pt
✅ 固定测试图片已保存
✅ 固定测试 Tensor 已保存
固定 Tensor 范围: [0.000, 1.000]
步骤3:完整的推理脚本(基准)
# inference_baseline.py - PyTorch 推理基准
import torch
import time
# 加载模型
model = SimpleCNN(num_classes=10)
model.load_state_dict(torch.load("simple_cnn.pth"))
model.eval()
# 加载测试输入
input_tensor = torch.load("test_input_fixed.pt")
# 推理
with torch.no_grad():
output = model(input_tensor)
print(f"输入: {input_tensor.shape}")
print(f"输出: {output.shape}")
print(f"输出值: {output[0]}")
print(f"预测类别: {output[0].argmax().item()}")
print(f"最大值: {output[0].max().item():.4f}")
# 性能测试
n_times = 100
start = time.time()
for _ in range(n_times):
with torch.no_grad():
_ = model(input_tensor)
end = time.time()
print(f"\n平均推理时间: {(end-start)/n_times*1000:.2f} ms")
print(f"FPS: {n_times/(end-start):.2f}")
运行:
python3 inference_baseline.py
输出:
输入: torch.Size([1, 3, 64, 64])
输出: torch.Size([1, 10])
输出值: tensor([ 0.0123, -0.0456, 0.0789, ...])
预测类别: 5
最大值: 0.1234
平均推理时间: 2.34 ms
FPS: 427.35
这个输出是基准,后续所有转换都要对比这个结果!
🎯 追踪脚本追踪的是什么?
核心对象:数据流的每一步转换
步骤1:PyTorch 模型
追踪对象:simple_cnn.pth
验证:模型能否加载
工具:python3 -c "import torch; model=torch.load('simple_cnn.pth')"
步骤2:PyTorch 推理
追踪对象:test_input_fixed.pt + simple_cnn.pth
验证:输出是否正确
工具:python3 inference_baseline.py
基准输出:预测类别=5, 最大值=0.1234
步骤3:ONNX 转换
追踪对象:simple_cnn.pth → simple_cnn.onnx
验证:ONNX 模型是否有效
工具:onnx.checker.check_model()
步骤4:ONNX 推理
追踪对象:test_input_fixed.pt + simple_cnn.onnx
验证:输出是否与 PyTorch 一致
工具:onnxruntime.InferenceSession()
对比:|output_onnx - output_pytorch| < 1e-5
步骤5:TensorRT 转换
追踪对象:simple_cnn.onnx → simple_cnn.engine
验证:engine 是否构建成功
工具:trt.Builder
步骤6:TensorRT 推理
追踪对象:test_input_fixed.pt + simple_cnn.engine
验证:输出是否与 PyTorch 一致
工具:tensorrt.Runtime
对比:|output_trt - output_pytorch| < 1e-3
🔧 现在可以写追踪脚本了
第一个追踪脚本:PyTorch → ONNX
#!/bin/bash
# trace_pytorch_to_onnx.sh - 追踪 PyTorch 转 ONNX
echo "🔍 追踪 PyTorch → ONNX..."
# 1. 检查输入文件
echo "[1/5] 检查输入文件..."
ls -lh simple_cnn.pth test_input_fixed.pt
# 2. PyTorch 基准推理
echo "[2/5] PyTorch 基准推理..."
python3 inference_baseline.py > baseline_output.txt
cat baseline_output.txt
# 3. 转换为 ONNX
echo "[3/5] 转换为 ONNX..."
python3 << EOF
import torch
from simple_model import SimpleCNN
model = SimpleCNN(num_classes=10)
model.load_state_dict(torch.load("simple_cnn.pth"))
model.eval()
dummy_input = torch.load("test_input_fixed.pt")
torch.onnx.export(
model,
dummy_input,
"simple_cnn.onnx",
export_params=True,
opset_version=17,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
verbose=False
)
print("✅ ONNX 导出成功")
EOF
# 4. 验证 ONNX 模型
echo "[4/5] 验证 ONNX 模型..."
python3 << EOF
import onnx
model = onnx.load("simple_cnn.onnx")
onnx.checker.check_model(model)
print("✅ ONNX 模型有效")
EOF
# 5. ONNX 推理并对比
echo "[5/5] ONNX 推理..."
python3 << EOF
import onnxruntime as ort
import torch
import numpy as np
# 加载模型
session = ort.InferenceSession("simple_cnn.onnx")
input_tensor = torch.load("test_input_fixed.pt").numpy()
# 推理
outputs = session.run(None, {'input': input_tensor})
output_onnx = outputs[0]
# 读取基准
with open("baseline_output.txt") as f:
for line in f:
if "预测类别:" in line:
baseline_class = int(line.split(":")[1].strip())
if "最大值:" in line:
baseline_max = float(line.split(":")[1].strip())
# 对比
onnx_class = output_onnx.argmax()
onnx_max = output_onnx.max()
print(f"PyTorch: 类别={baseline_class}, 最大值={baseline_max:.4f}")
print(f"ONNX: 类别={onnx_class}, 最大值={onnx_max:.4f}")
print(f"差异: 类别={'✅ 一致' if baseline_class==onnx_class else '❌ 不一致'}")
diff = np.abs(output_onnx.flatten() - torch.load("test_input_fixed.pt").numpy()).max()
print(f"数值差异: {diff:.6f}")
EOF
echo "✅ 追踪完成!"
💡 为什么这个"最简单的东西"重要?
作为"探针"的作用
✅ 固定输入:知道输入是什么
✅ 固定输出:知道正确输出是什么
✅ 简单模型:能理解每一步在做什么
✅ 可验证:每一步都能对比基准
就像电路板的"测试点":
- 测试点1:输入是否正确?
- 测试点2:模型加载成功?
- 测试点3:ONNX 转换正确?
- 测试点4:TensorRT 转换正确?
- 测试点5:最终输出是否一致?
🚀 现在可以开始学习了
第一步:运行最小示例
# 1. 创建模型
python3 simple_model.py
# 2. 创建测试输入
python3 create_test_input.py
# 3. 运行基准推理
python3 inference_baseline.py
# 记录输出!这是后续所有对比的基准
第二步:转换并验证
# 4. PyTorch → ONNX
python3 convert_to_onnx.py
# 5. ONNX 推理并对比
python3 inference_onnx.py
# 6. 检查:ONNX 输出是否与 PyTorch 一致?
第三步:优化并验证
# 7. ONNX → TensorRT
python3 convert_to_tensorrt.py
# 8. TensorRT 推理并对比
python3 inference_tensorrt.py
# 9. 检查:TensorRT 输出是否与 PyTorch 一致?
📊 完整数据流追踪表
步骤 文件 输入 输出 验证方法
──────────────────────────────────────────────────────────────────
1 simple_model.py - simple_cnn.pth ls -lh
2 create_test_input - test_input.pt torch.load
3 baseline.py simple_cnn.pth output_baseline 记录输出
4 to_onnx.py simple_cnn.pth simple_cnn.onnx onnx.checker
5 onnx_inference.py simple_cnn.onnx output_onnx 对比baseline
6 to_tensorrt.py simple_cnn.onnx simple_cnn.engine trt.Builder
7 trt_inference.py simple_cnn.engine output_trt 对比baseline
每一步都有明确的输入、输出、验证方法!
现在你有这个"最简单的东西"了,追踪脚本追踪的就是这个数据流的每一步转换。
想先运行一下这个最小示例吗?