1. 实战目标:用一个固定预算检验三条优化路线
所有优化都应围绕同一套产品预算展开,而不是分别追求最小模型或最高 Token/s。
假设最终场景是工业现场或机器人边缘节点,业务要求如下:
| 项目 | 目标 |
|---|---|
| 硬件平台 | Jetson AGX Orin |
| 模型权重 | 不超过统一内存的可用范围 |
| 推理方式 | 本地离线运行 |
| 大模型能力 | 支持多轮问答和工具调用 |
| CV 模型能力 | 满足目标检测延迟与精度 |
| 功耗 | 符合产品功耗模式 |
| 质量底线 | 不得低于业务验收阈值 |
| 评测口径 | 质量、时延、内存、功耗联合验收 |
由于 Orin 采用统一内存,模型权重不能占满全部容量。实际内存预算至少还要留给:
- 操作系统和显示服务;
- CUDA、TensorRT 运行时;
- Python 推理服务;
- KV Cache;
- 图像预处理;
- 后处理;
- ROS 或其他后台进程;
- 峰值激活和内存碎片。
因此,"模型装得下"只是起点,"长上下文和并发条件下依然稳定"才是验收重点。
1.1 三条路线分别对应不同约束
- 只量化
- 快速验证低精度路径;
- 适合模型结构已经固定;
- 主要考察量化质量损失。
- 量化后蒸馏
- 降低学生模型对大结构的依赖;
- 更适合目标场景明确的固定业务;
- 需要承担数据、训练和评测成本。
- 蒸馏后量化
- 同时优化模型结构和数值精度;
- 最终目标是端侧稳定部署;
- 必须验证误差叠加问题。
此次实战采用第三种路线:先证明量化基线,再用蒸馏生成更小的学生模型,最后完成 INT8/INT4 部署。
2. 实验环境:先冻结硬件和软件栈
在 Orin 上做实验,第一原则是保证环境可复现。软件版本、功耗模式和散热条件都属于实验变量。
2.1 推荐记录的环境字段
| 类别 | 记录内容 |
|---|---|
| 硬件 | Jetson SKU、内存容量 |
| 系统 | JetPack、Linux 内核版本 |
| CUDA | 主版本与驱动版本 |
| TensorRT | 版本与构建工具版本 |
| 推理框架 | 版本与构建参数 |
| Python | 解释器与依赖版本 |
| 功耗模式 | MAXN、MODE_30W 等 |
| 散热 | 风扇策略、机箱是否封闭 |
| 并发条件 | 单请求或多请求 |
| 输入条件 | 图像分辨率、上下文长度 |
| 后台服务 | 相机、ROS、显示等 |
建议通过以下命令记录关键信息:
# 查看 JetPack 与 L4T 信息
cat /etc/nv_tegra_release
# 查看系统内存
free -h
# 查看 CPU、内存和进程状态
top -b -n 1 | head -30
# 查看磁盘空间,避免引擎构建失败
df -h
# 查看当前功耗模式
sudo nvpmodel -q
# 记录设备时钟与频率
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
cat /sys/class/devfreq/17000000.ga10b/cur_freq
不同功耗模式下,Orin 的 CPU、GPU 频率和持续性能差异很大。因此,实验结果不能脱离功耗模式单独解释。
2.2 统一输出目录
mkdir -p \
~/edge_optimize/models \
~/edge_optimize/engines \
~/edge_optimize/calib \
~/edge_optimize/logs \
~/edge_optimize/results
日志中至少记录:
- 启动时间;
- 引擎构建耗时;
- 模型加载耗时;
- 首 Token 延迟;
- 每轮生成速度;
- 峰值内存;
- 平均温度;
- 输入和输出长度;
- 是否发生 OOM 或异常。
3. 大模型量化基线:用 llama.cpp 验证 INT4 是否真正有效
量化基线的目的不是选一个最小的 GGUF,而是比较质量、权重体积、内存和生成速度。
3.1 从源码构建并启用 CUDA
以 llama.cpp 为例,构建时至少启用 CUDA,并指定 Orin 的 GPU 架构:
git clone https://github.com/ggerganov/llama.cpp.git
cd llama.cpp
cmake -B build \
-DGGML_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES=87 \
-DGGML_CUDA_FA_ALL_QUANTS=ON \
-DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)" --target llama-server
关键参数含义:
GGML_CUDA=ON:启用 CUDA 后端;CMAKE_CUDA_ARCHITECTURES=87:对应 Orin 的 GPU 架构;GGML_CUDA_FA_ALL_QUANTS=ON:为更多量化类型的 KV Cache 启用 Flash Attention。
不同版本的构建参数、环境变量和支持范围可能发生变化。执行前应先核对官方 README。
3.2 下载并核对模型
选择目标模型后,应检查:
- 模型架构;
- 分词器;
- 上下文长度;
- 文件总大小;
- GGUF 版本;
- 量化元数据;
- 许可证;
- 是否需要远程代码。
目录结构可参考:
~/edge_optimize/models/
├── qwen2.5-7b-fp16.gguf
├── qwen2.5-7b-q8_0.gguf
├── qwen2.5-7b-q5_k_m.gguf
└── qwen2.5-7b-q4_k_m.gguf
不能仅凭文件名判断精度。正式实验前,应使用框架命令或模型工具核对 GGUF 的实际张量类型和量化版本。
3.3 启动兼容 OpenAI 协议的本地服务
cd ~/llama.cpp
build/bin/llama-server \
-m ~/edge_optimize/models/qwen2.5-7b-q4_k_m.gguf \
-ngl 999 \
-fa 1 \
--ctx-size 4096 \
--host 0.0.0.0 \
--port 8080
参数含义:
| 参数 | 作用 |
|---|---|
-m |
指定模型路径 |
-ngl 999 |
尽可能将模型层卸载到 GPU |
-fa 1 |
启用 Flash Attention |
--ctx-size |
设置上下文长度 |
--host |
服务监听地址 |
--port |
服务端口 |
-ngl 999 只是"尽可能卸载",并不意味着模型一定完全运行在 GPU 上。在统一内存和量化模型下,还要结合日志、内存和推理速度综合判断。
如果设备需要同时运行视觉模型或 ROS,可以逐步降低 GPU 层数,观察 CPU、GPU 和内存之间的实际平衡。
3.4 用真实业务数据请求服务
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2.5-7b",
"messages": [
{"role": "system", "content": "你是一名工业设备诊断助手,只输出 JSON。"},
{"role": "user", "content": "设备温度异常升高,请给出可能原因和检查步骤。"}
],
"temperature": 0.1
}'
客户端需要记录:
- 请求开始时间;
- 首 Token 返回时间;
- 每个 Token 的到达时间;
- 最终完成时间;
- 输入和输出 Token 数;
- HTTP 状态与异常。
只统计服务器日志中的平均 Token/s,会遗漏排队、网络传输和客户端处理时间,也无法真实反映用户感知。
3.5 基线条目示例
下表是一个实验记录模板,不代表跨设备通用性能:
| 模型与精度 | 权重体积 | 峰值内存 | TTFT | 生成速度 | 质量结论 |
|---|---|---|---|---|---|
| 7B FP16 | 大 | 高 | 基线 | 基线 | 质量基线 |
| 7B Q8_0 | 约 FP16 的一半 | 中高 | 待实测 | 待实测 | 优先比较 |
| 7B Q5_K_M | 更小 | 中 | 待实测 | 待实测 | 质量与体积平衡 |
| 7B Q4_K_M | 约 FP16 的四分之一 | 较低 | 待实测 | 待实测 | 端侧优先验证 |
实验结论应分为三层:
- 能装下:模型权重和运行时没有 OOM;
- 能运行:单请求能够稳定完成;
- 能有效运行:质量、TTFT、生成速度和功耗均满足产品目标。
如果 Q4 比 Q8 的模型体积小,但反量化计算更多,实际解码速度未必更高。因此,权重体积和生成速度不能画等号。
4. CV 模型量化:从 ONNX 到 INT8 TensorRT 引擎
CV 模型的量化比 LLM 多一个校准环节,核心是用真实输入确定激活范围。
4.1 先验证 FP16 基线
以目标检测模型为例,可先将 PyTorch 模型导出为 ONNX:
from ultralytics import YOLO
model = YOLO("best.pt")
model.export(
format="onnx",
imgsz=640,
opset=17,
dynamic=False,
simplify=True,
batch=1
)
执行前需要确认:
- 目标推理框架是否支持对应 opset;
- 动态尺寸是否会带来额外开销;
- 导出模型是否包含训练节点;
- 输入尺寸是否与真实采集分辨率一致;
- Batch Size 是否匹配实际请求方式。
之后用 TensorRT 构建 FP16 引擎:
/usr/src/tensorrt/bin/trtexec \
--onnx=best.onnx \
--saveEngine=best_fp16.engine \
--fp16 \
--workspace=4096
不要跳过 FP16 基线。INT8 变快但精度明显下降,不能直接得出"INT8 不好"的结论,还需要检查校准数据、输入范围和异常算子。
4.2 INT8 校准的关键是数据代表性
建议建立三类校准数据:
| 类别 | 示例 | 目的 |
|---|---|---|
| 典型数据 | 正常光照、标准距离 | 覆盖日常业务分布 |
| 困难数据 | 夜间、逆光、遮挡 | 验证边界情况 |
| 现场数据 | 与部署环境一致 | 减少场景分布偏移 |
校准数据不需要非常大,但必须能代表真实输入。如果采集数据与实际场景差异明显,即使校准过程没有报错,INT8 引擎也可能在生产中出现系统性退化。
4.3 构建并测试 INT8 引擎
/usr/src/tensorrt/bin/trtexec \
--onnx=best.onnx \
--saveEngine=best_int8.engine \
--int8 \
--calib=calibration_cache.bin \
--workspace=4096
随后在同一组真实图像上进行回归测试:
import time
import cv2
from ultralytics import YOLO
model = YOLO("best_int8.engine")
image = cv2.imread("test.jpg")
# 预热
for _ in range(10):
model.predict(image, verbose=False)
start = time.perf_counter()
for _ in range(100):
model.predict(image, verbose=False)
end = time.perf_counter()
latency_ms = (end - start) / 100 * 1000
print(f"平均推理时延:{latency_ms:.2f} ms")
print(f"平均吞吐:{1000 / latency_ms:.2f} FPS")
测试要求:
- 预热后再计时;
- 多次运行并去掉首轮异常值;
- 使用真实图像分辨率;
- 同时记录 mAP、Precision、Recall;
- 记录 CPU、GPU、温度和功耗;
- 避免后台任务不稳定干扰结果。
4.4 精度下降后的排查顺序
- 检查校准集
- 是否覆盖困难样本;
- 是否与真实分布一致;
- 输入尺寸和归一化是否相同。
- 检查敏感层
- 识别对低精度更敏感的算子;
- 对关键层保持较高精度;
- 避免全局无差别量化。
- 检查数据范围
- 是否存在输入未归一化;
- 是否存在异常极大值;
- 校准统计量是否被少量样本主导。
- 检查算子兼容性
- 是否存在算子回退;
- 是否存在精度转换;
- 是否存在动态形状带来的重优化。
- 回退精度或调整模型
- 从 INT8 回退至 FP16;
- 对关键分支局部保持高精度;
- 通过蒸馏缩小模型,而不是进一步压低精度。
量化不是精度下降后的唯一解决方案。对于对低精度敏感的任务,保留部分关键路径的高精度,往往比全局使用更低精度更合理。
5. 蒸馏实践:让小模型继承教师的判断边界
蒸馏的价值不只是缩小参数规模,而是让小模型学习教师的输出分布、中间特征和推理行为。
5.1 设计评测集比选择损失函数更重要
评测集至少应包含以下四类数据:
- 常规问答
- 验证基本指令跟随能力。
- 工具调用
- 验证 JSON 格式、字段名和参数是否稳定。
- 领域判断
- 验证目标行业的术语与业务规则。
- 边界拒答
- 验证模型是否会在信息不足时随意作答。
每一类都应同时设置:
- 输入提示;
- 预期输出格式;
- 必须包含的信息;
- 必须避免的错误;
- 可接受答案范围;
- 难度等级。
如果评测集只包含开放式问答,就无法发现蒸馏后模型在格式、数值和工具调用上的退化。
5.2 一个简化版蒸馏训练脚本
以下示例用于说明训练结构,实际执行时需要根据模型架构、分词器、并行策略和硬件条件调整:
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader
def distillation_loss(
student_logits,
teacher_logits,
labels,
temperature=2.0,
alpha=0.5
):
# 学生模型同时学习教师输出分布和真实标签
soft_target = F.softmax(teacher_logits / temperature, dim=-1)
soft_log = F.log_softmax(student_logits / temperature, dim=-1)
distill = F.kl_div(soft_log, soft_target, reduction="batchmean") * (temperature ** 2)
label_loss = F.cross_entropy(
student_logits.view(-1, student_logits.size(-1)),
labels.view(-1)
)
return alpha * distill + (1 - alpha) * label_loss
需要特别区分:
temperature只在生成软标签和计算蒸馏损失时使用;- 最终部署时,模型生成温度属于服务配置;
- 教师模型的输出不应直接照搬到所有困难样本中。
5.3 大模型蒸馏建议采用离线教师生成
离线生成流程为:
原始问题
→ 教师模型生成软标签
→ 学生模型训练
→ 检查点选择
→ 目标硬件评测
优点是教师推理与训练解耦,便于:
- 缓存教师输出;
- 复用不同学生模型;
- 对困难样本追加采样;
- 筛选高质量训练数据;
- 控制训练成本和随机性。
对于目标检测模型,可以让学生模型学习:
- 教师的分类概率分布;
- 边界框回归结果;
- 特征图表示;
- 困难样本的置信度;
- 小目标、遮挡和边缘样本的判断方式。
5.4 学生模型要小到什么程度?
不能只根据参数数量决定。需要联合评估:
| 维度 | 学生模型必须满足条件 |
|---|---|
| 结构 | 能够被目标运行时高效支持 |
| 精度 | INT8/INT4 后仍有可用质量 |
| 内存 | 权重、激活、KV Cache 均可容纳 |
| 时延 | 满足单请求或并发目标 |
| 功耗 | 在目标功耗模式下持续运行 |
| 业务 | 能够保留目标场景所需能力 |
| 维护 | 后续数据和版本可以持续迭代 |
一个更合理的目标是:
学生模型不是"全面逼近教师",而是在目标场景中保留足够能力,并把参数、算子和数值表示调整到适合 Orin 的程度。
6. Orin 全链路验证:从能跑到可交付
全链路验证必须覆盖启动、加载、推理、持续运行和资源回收,而不是只测试一次推理。
6.1 建立统一测试脚本
import time
import requests
BASE_URL = "http://localhost:8080/v1/chat/completions"
prompt = "根据设备日志判断是否存在过热风险,并输出 JSON。"
payload = {
"model": "edge-student",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"stream": True
}
headers = {"Content-Type": "application/json"}
start = time.perf_counter()
first_token_time = None
token_count = 0
content = ""
with requests.post(BASE_URL, json=payload, headers=headers, stream=True) as resp:
for line in resp.iter_lines():
if not line:
continue
token_count += 1
if first_token_time is None:
first_token_time = time.perf_counter()
content += line.decode("utf-8", errors="ignore")
end = time.perf_counter()
ttft = first_token_time - start if first_token_time else None
total_time = end - start
print(f"TTFT:{ttft:.3f} s")
print(f"总耗时:{total_time:.3f} s")
print(f"Token 数量:{token_count}")
print(f"生成速度:{token_count / total_time:.2f} token/s")
输出示例:
TTFT:0.85 s
总耗时:4.20 s
Token 数量:126
生成速度:30.00 token/s
这些数字只能在当前模型、上下文长度、输入内容和设备状态下解释,不能直接推广到其他模型。
6.2 内存测试要覆盖最坏情况
测试矩阵可包括:
| 上下文长度 | 并发数 | 模型精度 | 是否同时运行 CV 模型 |
|---|---|---|---|
| 2048 | 1 | FP16 | 否 |
| 4096 | 1 | INT8 | 否 |
| 4096 | 2 | INT4 | 是 |
| 8192 | 1 | INT4 | 是 |
KV Cache 会随上下文长度增长。多轮对话不应只测试第一条消息,而应持续追加历史,观察:
- 每轮内存增长;
- 每轮 TTFT 变化;
- 每轮生成速度变化;
- 长上下文下是否发生 OOM;
- 服务是否及时回收资源。
6.3 持续压测决定功耗模式
建议连续运行:
- 10 分钟观察性能抖动;
- 30 分钟观察温度与频率;
- 峰值负载观察功耗上限;
- 恢复空闲观察资源回收。
记录字段包括:
- 平均 Token/s;
- 峰值 Token/s;
- TTFT 中位数和 P95;
- 内存峰值;
- CPU、GPU 占用;
- 设备温度;
- 平均功耗;
- 是否发生降频;
- 是否发生 OOM。
如果短时运行速度很高,但连续运行 30 分钟后明显下降,说明性能可能受散热或持续功耗限制。
6.4 验收门槛应按产品要求填写
| 指标 | 基线 | 目标 | 实测 | 是否通过 |
|---|---|---|---|---|
| 任务质量 | 教师模型 | 下降不超过预设阈值 | 待填写 | □ |
| 首 Token 延迟 | FP16 | 低于目标值 | 待填写 | □ |
| 生成速度 | FP16 | 不低于目标值 | 待填写 | □ |
| 峰值内存 | FP16 | 低于安全水位 | 待填写 | □ |
| 长上下文稳定性 | FP16 | 不发生 OOM | 待填写 | □ |
| 30 分钟持续性能 | FP16 | 无明显降频 | 待填写 | □ |
| 平均温度 | 硬件阈值 | 低于安全阈值 | 待填写 | □ |
| 工具调用正确率 | 教师模型 | 达到验收要求 | 待填写 | □ |
质量门槛必须排在第一位。一个错误率上升但速度更快的方案,在自动化决策、机器人或工业场景中通常不具备交付价值。
7. 组合优化路线:如何决定何时停止
优化过程应当逐层推进,而不是一次性同时修改模型结构、精度、运行时和硬件配置。
7.1 第一步:量化现有模型
优先测试:
- FP16/BF16 质量基线;
- INT8 质量与速度;
- INT4 权重方案;
- INT8 权重、INT4 缓存等组合;
- KV Cache 量化后的内存变化。
如果 INT4 模型文件更小,但解码速度与 INT8 接近,应重点检查:
- 反量化是否引入额外计算;
- GPU 是否真正支持对应低精度路径;
- 生成速度是否受权重读取或计算限制;
- 是否只有权重被压缩,缓存没有同步优化。
7.2 第二步:决定是否需要蒸馏
满足以下条件时,可以暂缓蒸馏:
- 质量损失在可接受范围内;
- 模型内存仍有余量;
- 单请求时延达到目标;
- 功耗和散热稳定;
- 模型不需要频繁升级;
- 蒸馏数据成本高于收益。
出现以下情况时,蒸馏更值得投入:
- 时延或内存无法满足;
- 模型必须长期演进;
- 目标场景和输入分布已经明确;
- 固定模型已经没有进一步压缩空间;
- 需要同时降低结构复杂度和推理成本。
7.3 第三步:蒸馏后重新量化
蒸馏后不能直接复用上一轮量化结论,因为学生模型的:
- 权重分布可能不同;
- 激活范围可能不同;
- 对误差的敏感度可能不同;
- 网络结构可能已经变化。
应在学生模型上重新完成:
- 教师输出生成;
- 蒸馏训练;
- INT8 校准;
- GGUF 或 TensorRT 转换;
- Orin 真实负载测试;
- 质量与性能回归。
7.4 第四步:生产化
生产化至少包括:
- 固定运行时版本;
- 缓存构建好的引擎;
- 使用健康检查;
- 设置请求超时;
- 设置最大上下文长度;
- 监控内存和温度;
- 异常时降级或返回明确错误;
- 保留模型版本和评测记录;
- 对关键服务设置重启和资源回收策略。
引擎构建通常只需要在部署阶段完成一次,但模型、JetPack 或运行时发生变化时,必须重新验证。
8. 结果分析:不能直接相信任何单点数据
单点性能只能说明当时、当地、特定负载下的结果,不能替代完整验收。
不同测试中的 Token/s、内存、温度和功耗,可能受到以下因素影响:
- Jetson SKU 与内存容量;
- JetPack、CUDA、TensorRT 版本;
- llama.cpp 或 TensorRT-LLM 构建参数;
- 模型架构、词表和上下文长度;
- 模型文件类型;
- 输入与输出长度;
- 单请求或多并发;
- 功耗模式;
- 散热方式;
- 是否同时运行相机、ROS 或其他服务。
因此,此次实操中的命令和实验模板主要用于建立流程,不能直接替代性能承诺。真正有效的验收必须包含:
版本信息
+ 模型信息
+ 输入负载
+ 硬件状态
+ 内存数据
+ 速度数据
+ 温度与功耗
+ 业务质量结论
只有这些字段同时存在,测试结果才具备可复现性。
9. 结论:先验证量化,再决定是否蒸馏
Orin 上的最优路线通常不是"量化一次、蒸馏一次"就结束,而是把量化和蒸馏视为两个可以分别验证、也可以联合执行的决策点。
流程如下:
-
在目标 Orin 和固定 JetPack 版本上建立 FP16/BF16 质量基线;
-
量化同一模型,比较 INT8 和 INT4 的质量、内存与生成速度;
-
只有当质量可接受、但时延、内存或功耗仍不满足时,再引入蒸馏;
-
对学生模型重新完成量化和 Orin 全链路测试;
-
将最终引擎、质量门槛、性能报告和资源监控一并纳入生产化交付。
可接受的答案质量
- 可预测的推理速度
- 受控的内存占用
- 稳定的功耗和散热
- 可持续维护的数据与模型版本
这才是端侧模型压缩的真正目标。