手上这台 Xavier NX 在工位上蹲了快一年,主要用来跑边缘侧的 Agent 验证:本地小模型做意图理解和工具调用,再加一路视觉做目标检测,整套东西要塞进十几瓦的功耗预算里。系统被我前后 apt 升级过好几轮,CUDA、TensorRT、OpenCV 的版本互相打架,最后决定干脆推平重刷。这次选的是 JetPack 5.1.7,也就是 Xavier 系列长期维护线上的一个较新版本。刷机这件事听起来和给机顶盒刷固件、给安卓板子刷包是一个逻辑——把镜像写进设备的存储里——但 Jetson 这套工具链完全是另一套玩法:宿主机的 SDK Manager、recovery 模式、QSPI 引导固件、initrd flash,每一步都有各自的坑。这篇文章写给三类人:手上刚拿到 Xavier NX 不知道怎么下手的;设备能跑但环境已经被折腾乱、想推平重来的;以及准备在这块板子上跑 Agent 推理、想知道内存和存储该怎么规划的。整个流程我会按我实际操作顺序走一遍,参数、命令、报错和排查过程都留在里面,能直接抄。
1. 先把需求拆清楚:为什么这台机器值得重刷一遍
1.1 边缘 Agent 对这台机器的真实诉求
很多人对"Agent"这个词的理解还停留在网页里那个对话框,但一旦落地到 Xavier NX 这种边缘设备上,Agent 就变成了一套很具体的工程组合:感知输入(摄像头、麦克风、传感器)、本地推理(小参数量的语言模型或视觉模型)、工具调用(控制 GPIO、发请求、读写本地数据)、以及一层薄薄的记忆(本地文件或小向量库)。这套东西跑在 8GB 或 16GB 的统一内存上,GPU 和 CPU 共享同一块 LPDDR4x,任何一个环节多占几百兆,别的环节就得往后让。
Xavier NX 的硬件底子是:6 核 Carmel ARM v8.2 CPU、384 核 Volta GPU 加 48 个 Tensor Core、128 位 LPDDR4x、模块上焊死 16GB eMMC 5.1、算力标称 21 TOPS(INT8)。计算能力对应 compute capability 7.2,这意味着 JetPack 5.x 这条线上的 CUDA 11.4、TensorRT 8.5 是它的最优搭配。你要在这块板子上同时跑 YOLO 类的检测模型和一个 1.5B 到 3B 的量化语言模型,内存和散热余量其实非常紧张,所以系统底座的干净程度直接影响后面能不能稳。
我这次重刷的直接原因有三个:一是 apt 升级过程中有几个 L4T 相关的包被降级过,/etc/nv_tegra_release显示的版本和实际安装的 CUDA 包对不上;二是 eMMC 只剩 2GB 空间,装个 PyTorch wheel 都要先删东西;三是 TensorRT 的 engine 反序列化偶尔报版本不匹配。这三个问题单独看都能绕过去,但叠在一起就没法做可复现的验证了。
1.2 什么时候必须重刷,什么时候 apt 就够了
这里得先说清楚一件事:不是所有环境问题都需要推平。如果你只是想把 JetPack 从 5.1.4 升到 5.1.7,而且 rootfs 本身没坏、分区表没动过,那直接走 apt 升级路径就行,省下一两个小时:
sudo apt update sudo apt dist-upgrade sudo apt install nvidia-jetpack升级完重启,然后核对三处信息:cat /etc/nv_tegra_release看 L4T 版本,dpkg -l | grep -E "cuda|tensorrt"看组件版本,jetson_release(装了 jetson-stats 的话)看整体汇总。这条路径的前提是你没动过 QSPI 引导区、没换过根文件系统所在的设备。
而下面这几种情况,我建议直接重刷,别修:
- 根文件系统从 eMMC 迁移到 NVMe,或者反过来,这类涉及分区表和引导链的变更,靠 apt 是搞不定的。
- 想从 SD 卡启动的版本切到 eMMC 版本(板级配置
jetson-xavier-nx-devkit和jetson-xavier-nx-devkit-emmc是两个不同的 target,不能混用)。 - 系统里被手动装过非官方源的 CUDA 或驱动,包依赖已经乱了。
- 要给多台设备做统一基线,需要一个"从零到可用"的确定流程。
还有一点值得提前知道:按目前的产品路线,JetPack 6 系列只覆盖 Orin 家族,Xavier 这条线停在 5.1.x 做长期维护。也就是说 5.1.7 很可能不是你最后一次刷,而是这条线上一个相对稳定的落点。既然是长期落点,就值得把这次的流程整理成可复用的脚本和文档,而不是每次凭记忆敲。
1.3 JetPack 5.1.7 里到底装了什么
JetPack 不是一个单一软件,它是 L4T(Linux for Tegra,底层 BSP 和内核)加一堆上层 SDK 的打包。5.1 这条维护线的组件版本大致如下,具体数字以你下载到的那份 release notes 为准,因为维护版本之间会有小幅跳动:
| 组件 | 版本(5.1 维护线) | 在 Agent 场景里的作用 |
|---|---|---|
| L4T / BSP | r35.6.x,内核 5.10 | 板级驱动、设备树、电源管理 |
| CUDA | 11.4 | 自编译算子、llama.cpp 的 CUDA 后端 |
| cuDNN | 8.6.x | 视觉模型里的卷积加速 |
| TensorRT | 8.5.x | ONNX 转 engine,推理主力 |
| VPI | 2.3 | 图像预处理、光流等 |
| OpenCV | 4.5.4(带 CUDA 支持) | 取流、缩放、颜色空间转换 |
| Multimedia API | 35.6 | 硬编解码、零拷贝取流 |
| DeepStream | 6.2(可选) | 多路视频分析流水线 |
对跑 Agent 的人来说,最关键的是 CUDA 和 TensorRT 这两个版本号。你后面从 pip 装的 PyTorch wheel、从源码编的 torchvision、以及 TensorRT engine 文件,都必须和这两个版本对齐。这也是为什么我强烈建议:重刷之前先把这套版本号记下来,贴在你的项目 README 里,半年后你会感谢自己。
另外提醒一句,nvidia-jetpack这个元包可以让你一条命令装齐所有组件,但它会顺带拉一堆你可能用不到的东西(比如 Nsight 全家桶、VPI 的样例、各种文档包)。如果 eMMC 空间紧张,可以只装子包,比如nvidia-jetpack-dev加nvidia-cuda、nvidia-tensorrt,或者干脆刷机的时候在 SDK Manager 里不勾那些可选项。
2. 刷机前的准备:宿主机、线材和磁盘规划
2.1 宿主机怎么选,SDK Manager 还是纯命令行
刷机的宿主机必须是 x86_64 的 Ubuntu。官方支持的是 20.04 和 22.04 这两个 LTS 版本,其他版本能不能用纯看运气——我在 23.10 上试过一次,SDK Manager 的图形界面能起来,但下载完固件之后卡在解压环节,报的错还特别含糊。物理机和虚拟机都行,虚拟机需要把 USB 设备直通进去,刷机过程中不能断开。WSL2 我不推荐,USB 转接和 recovery 设备识别这两步的折腾成本远高于装个双系统。
如果你的日常开发机是 Windows 或 macOS,最省事的方案是找一台吃灰的笔记本装 Ubuntu 20.04 物理机,或者用虚拟机但给它至少 8GB 内存和 150GB 磁盘。SDK Manager 的下载缓存和临时解压空间加起来,一次完整的 JetPack 5.1.7(含所有可选组件)要占掉 50 到 70GB,磁盘告急是刷机失败最常见的原因之一,而且报错文本往往不会告诉你是空间不够。
工具上你有两条路:
- SDK Manager(图形):一站式把固件烧进 eMMC,然后自动在目标机上装 SDK 组件。适合第一次上手,出问题的时候界面上的日志还算看得懂。
- 命令行(
flash.sh/l4t_initrd_flash.sh):需要你先自己把 BSP 包和根文件系统包下载解压,然后手动调脚本。适合做自动化、批量、以及要刷到 NVMe 的场景。
我自己的习惯是两条都用:第一次用 SDK Manager 走通全流程,确认硬件和线材没问题;然后从 SDK Manager 的下载目录里把 BSP 和 rootfs 的压缩包捞出来,后续所有操作都走命令行。这样重刷一次的时间能压到二十分钟以内,而且整个过程可以写成脚本。
2.2 硬件清单和几个必然踩的坑
准备工作里最容易被低估的是"线"和"电"。Xavier NX 开发套件的供电是 19V 直流圆口,外径 5.5mm 那一款,官方适配器是 19V/4.74A。别拿 12V 的适配器凑合,板子可能会亮但跑不起来 GPU 负载,或者刷到一半掉电。也别用功率不够的 PD 充电头转接,20W 模式下瞬时功耗能顶到 20W 以上,加外设之后余量很小。
数据线方面,刷机用的那根必须能走数据,不能是纯充电线。这根线的质量直接决定刷机成功率,我前后换过三根,最后留下一根带屏蔽层的短线专门刷机用。劣质线的表现很有迷惑性:lsusb能看到设备,但传到一半报 USB write error,让你以为是软件问题。
清单大致是这些:
- 宿主机一台(Ubuntu 20.04/22.04,x86_64,磁盘余量 150GB 以上)
- Xavier NX 开发套件或模块加载板,19V 原装或同规格适配器
- 一根质量可靠的 USB 数据线,接到载板的刷机口(开发套件上是那颗专用的 USB-C,早期批次有的载板是 micro-USB,按手上的板子来)
- 网线一根(后续装 SDK 组件或者走 OTA 会用到)
- 一根 USB 转 TTL 串口线,3.3V 电平(强烈建议备着,刷机失败的时候串口日志是唯一有效信息)
- 可选:M.2 2280 NVMe 固态,如果打算从 NVMe 启动
提示:串口线是这次刷机的救命稻草。开发套件上有一组调试 UART 排针,接 GND、TX、RX 三根就行(TX/RX 要交叉),波特率 115200 8N1。板子黑屏、卡在启动、刷完进不去系统的时候,
sudo picocom -b 115200 /dev/ttyUSB0一开,所有信息都在里面。
2.3 进入 recovery 模式的正确手法
这是整个流程里容错率最低的一步,也是最容易反复失败的一步。Xavier NX 开发套件的载板边缘有三个按钮:电源(PWR)、复位(RST)、强制恢复(REC)。标准手法是:
- 确保板子已经断电,拔掉电源。
- 按住 REC 键不要松。
- 插上电源(或者按一下 PWR),保持 REC 按住大约两秒。
- 松开 REC。
也有一种更稳的变体:先在通电状态下按住 REC,然后按一下 RST,松开 RST,等两秒再松开 REC。两种都可以,我更习惯第二种,因为不用反复插拔电源。
进入 recovery 模式成功的判据,在宿主机上执行lsusb,应该能看到一个 NVIDIA 的设备,ID 是0955:7020。如果看到的是别的一串数字,或者压根没有 NVIDIA 相关条目,说明没进对模式,或者线不对。这时候别急着怀疑软件,先把线换一根、USB 口换一个(直连主板后置口,别走前置面板或者 USB Hub)。
有个细节很多人不知道:recovery 模式是一次性的。只要宿主机开始向设备写数据、或者设备重新上电,就会退出 recovery。所以如果刷机中途失败,你必须重新走一遍上面的按键流程,不能指望它还在 recovery 里等着。
2.4 磁盘规划和目录结构
把宿主机上的目录先规划好,后面能省很多事:
# SDK Manager 的下载缓存,默认在这 ls ~/Downloads/nvidia/sdkm_downloads/ # 我习惯额外建一个工作目录,存放解压后的 BSP 和 rootfs mkdir -p ~/jetson/nx-5.1.7 && cd ~/jetson/nx-5.1.7完整的下载缓存目录里,你会看到类似JetPack_5.1.7_Linux_JETSON_XAVIER_NX_TARGETS这样的目录,里面有Linux_for_Tegra和一堆.tbz2的根文件系统压缩包。把这个目录整个软链到我的工作目录下,后续所有flash.sh操作都在这里执行。
顺手做件事:把当前的.conf配置、你改过的设备树、以及所有自定义的启动参数先备份出来。重刷会把 eMMC 上的东西全清掉,包括/etc/fstab里你可能加过的 NVMe 挂载项。我在第一次重刷的时候就吃过这个亏,刷完发现 SSD 没挂上,还以为是硬件坏了。
3. 两条刷机路线:图形化 SDK Manager 与命令行 initrd flash
3.1 SDK Manager 的完整操作流程
SDK Manager 的安装很简单,从官方页面下载 deb 包之后:
sudo apt install ./sdkmanager_2.x.x-xxxx_amd64.deb sdkmanager启动之后需要登录 NVIDIA 开发者账号,这个是必须的,没有账号走不下去。登录之后界面分三步:
Step 1选择目标硬件和系统版本。Hardware 选Jetson Xavier NX,注意这里有一个下拉项区分"模块 + 开发套件"和"模块 + 第三方载板",如果你用的是非官方载板,选后者并在后续手动指定板级配置。Target Operating System 选 JetPack 5.1.7,下面的 DeepStream、Isaac ROS 之类按需勾选,跑 Agent 的话 DeepStream 可以勾上,Isaac 一般用不到。
Step 2选择要装的组件。这一步有个大坑:左侧的 Host Components 里默认会勾上宿主机上的 CUDA、cuDNN 之类,如果你宿主机不是拿来训练的,全部取消勾选,只保留 Flash Jetson 相关的部分。右侧 Target Components 至少要留Jetson Runtime Components、CUDA、TensorRT、OpenCV、cuDNN。如果你的 eMMC 只有 16GB,我建议这里只留最核心的几个,剩下的后面用 apt 按需装。
Step 3开始烧写。这一步 SDK Manager 会先检查设备是否在 recovery 模式,然后再开始写。这里的手感是:先在界面上点开始,等它提示"请将设备置于 recovery 模式",这时候再去按按钮插线。顺序反过来也能用,但一旦设备被宿主机识别过一次,有时候会提示设备已连接但状态不对,得重新来一遍。
烧写过程中你会看到进度条走到 100%,然后它自动开始装 SDK 组件。装组件这一步需要设备已经启动并且能联网——通常是通过 USB 网络共享,SDK Manager 会在宿主机上创建一个虚拟网卡,给设备分配192.168.55.x的地址。如果这一步报网络超时,检查宿主机的防火墙是不是拦了这张虚拟网卡。
OEM 配置这步别跳过。它会让你设用户名、密码、主机名,这是设备第一次启动时的初始账号。密码我建议设一个你能记住但不简单的,因为后面 SSH、sudo 都要用。主机名用有意义的,比如nx-agent-01,多台设备的时候不会搞混。
3.2 命令行刷 eMMC 的完整参数说明
从 SDK Manager 的下载缓存里拿到Linux_for_Tegra目录之后,命令行刷机就是一行命令的事。刷模块上的 eMMC:
cd ~/jetson/nx-5.1.7/Linux_for_Tegra sudo ./flash.sh jetson-xavier-nx-devkit-emmc mmcblk0p1这里的两个参数分别是板级配置名和目标根文件系统分区。板级配置名必须和你的硬件匹配:模块加官方开发套件载板且从 eMMC 启动,用jetson-xavier-nx-devkit-emmc;如果你手上是 SD 卡启动的那个版本,配置名是jetson-xavier-nx-devkit。这两个名字差一个后缀,用错了会刷出一个起不来的系统。
这条命令背后做的事,简单说分四步:先把引导固件(BCT、MB1、各种固件 blob)写进模块的 QSPI 和 eMMC 的引导分区;然后把根文件系统镜像写到mmcblk0p1;接着生成设备树和内核的签名;最后校验写入内容。整个过程大概五到八分钟,取决于 USB 速度。
第一次刷完之后,根文件系统镜像会缓存成bootloader/system.img。后面如果只是要重装系统而不改配置,可以加-r参数复用这份镜像,速度快很多:
sudo ./flash.sh -r jetson-xavier-nx-devkit-emmc mmcblk0p1但要注意:如果你想改变根分区的大小,必须重新生成镜像,-r会让它沿用旧的尺寸。指定根分区大小的参数是-S,比如:
sudo ./flash.sh -S 28GiB jetson-xavier-nx-devkit-emmc mmcblk0p1这里的28GiB不是随便写的。eMMC 总容量 16GB 的模块上你写不了 28,这个是给从 NVMe 启动的场景用的。写之前先确认目标设备的容量,别让脚本把一个放不下的分区表写进去。
3.3 刷到 NVMe 并从 NVMe 启动
这是我这次重刷的核心诉求。原因很现实:模块上那块 16GB eMMC,装完 L4T 和基础组件就只剩七八个 G,再塞 CUDA 的样例、PyTorch wheel、几个量化模型,立刻就满了。而 eMMC 的写入速度也就那样,跑 Agent 的时候频繁读写小的状态文件,时间长了会明显拖慢。
Xavier NX 支持从 NVMe 启动,但流程不是"插上 SSD 直接刷"。它需要两步:先更新 QSPI 里的引导固件,让 bootloader 知道要去 NVMe 上找根文件系统;然后把根文件系统写到 NVMe 上。这两步可以一条命令搞定:
sudo ./tools/kernel_flash/l4t_initrd_flash.sh \ --external-device nvme0n1p1 \ -c tools/kernel_flash/flash_l4t_external.xml \ -p "-c bootloader/t186ref/cfg/flash_l4t_t194_qspi_p3668.xml" \ --showlogs --network usb0 \ jetson-xavier-nx-devkit-emmc internal参数逐个解释一下,因为这行命令里每个部分都有讲究:
--external-device nvme0n1p1告诉脚本根文件系统要写到哪里。如果你之前分区过 SSD,编号可能不是nvme0n1p1,先插到宿主机上确认一遍。-c tools/kernel_flash/flash_l4t_external.xml指定外部设备的分区布局模板。这个文件决定了 APP 分区多大、有没有预留其他分区。如果要用满整块盘,就编辑这个 xml。-p "-c bootloader/t186ref/cfg/flash_l4t_t194_qspi_p3668.xml"是给内部设备的 QSPI 引导配置。t194 是 Xavier 系列的平台代号,p3668 是模块的板型编号,这对组合和 Xavier NX 是对上的。--network usb0让刷机过程通过 USB 网络共享传输根文件系统,比纯 USB 传输稳定。- 最后的
internal表示根文件系统在外部设备、引导在内部设备。
整个过程分两个阶段:第一阶段刷 QSPI 引导,第二阶段通过 USB 网络把根文件系统推到 NVMe 上。整个耗时大概十五到二十五分钟,比刷 eMMC 长,因为传输的数据量大。
有一个隐私相关的坑顺嘴提一下(这和技术无关,但会实实在在地卡住流程):SDK Manager 和这套脚本都要求宿主机能正常解析 NVIDIA 的下载域名。有些公司内网会做 SSL 拦截,导致下载组件包的时候报证书错误。真遇到这种情况,找网络管理员要一下白名单,别自己去改证书校验。
注意:刷到 NVMe 之前,先确认 SSD 是好的、并且已经格式化过。我遇到过一次 SSD 上一块坏块正好落在分区表位置,刷机过程一切正常,但第一次启动时 U-Boot 找不到根分区,串口日志里反复刷
Waiting for root device。换一块盘就好了。
3.4 一次两段式刷机的省时做法
如果你要给多台同样配置的机器刷,或者需要反复调整根文件系统的内容,推荐用两段式:先生成镜像但不烧写,再单独烧写。这样镜像生成一次,后面可以反复用。
# 第一步:只生成镜像,不烧写 sudo ./tools/kernel_flash/l4t_initrd_flash.sh \ --external-device nvme0n1p1 \ -c tools/kernel_flash/flash_l4t_external.xml \ -p "-c bootloader/t186ref/cfg/flash_l4t_t194_qspi_p3668.xml" \ --showlogs --network usb0 --no-flash \ jetson-xavier-nx-devkit-emmc internal # 第二步:设备的 recovery 模式就绪后,只做烧写 sudo ./tools/kernel_flash/l4t_initrd_flash.sh --flash-only \ --network usb0 jetson-xavier-nx-devkit-emmc internal这个做法的价值在于,第一步生成镜像的时候你可以往 rootfs 里预置东西——比如预装好 docker、把模型文件塞进/opt/models、把开机自启的 systemd 服务写好。这样做出来的是一份"带内容的固件",刷完即用,省掉每台机器上重复的手工配置。
我实测下来,预置过的镜像刷一台机器大概十二分钟,纯手工从零配到能用要两个小时以上。设备数量超过三台,这套流程就绝对划算。
4. 刷完之后:让这台机器真的能跑起 Agent
4.1 首次启动和基础环境核对
第一次上电之后,先看串口有没有正常启动日志,再决定接不接 HDMI。如果串口能看到 U-Boot 的打印、内核的一系列[OK]、最后停在登录提示,说明系统起来了。如果 HDMI 一直黑屏但串口正常,多半是显示相关的配置问题,不影响 SSH。
连上之后第一件事是核对版本:
cat /etc/nv_tegra_release # 输出类似 R35 (release), REVISION: 6.x, ... dpkg -l | grep -E "cuda-toolkit|tensorrt|libcudnn|opencv" nvcc -V python3 --version/etc/nv_tegra_release里的 revision 就是 L4T 的版本号,和 JetPack 的对应关系要记住:JetPack 5.1 线对应 L4T 35.x,其中 5.1.7 落在 35.6 这个分支上。这个数字很重要,因为它决定了你后面能装哪些版本的 PyTorch wheel。
然后是磁盘和时间:
df -h sudo apt install -y chrony sudo systemctl enable --now chrony时间同步这件事看着不起眼,但设备断电一段时间之后时钟会漂,apt 和 pip 走 HTTPS 都会因为证书时间校验失败而报错,报错信息还完全看不出是时间问题。我第一次遇到的时候排查了半小时。
SSH 建议从第一次启动就配好,后面所有的操作都在宿主机上远程做,串口只在出问题的时候用:
sudo systemctl enable --now ssh ip -4 addr show如果走 USB 网络共享,设备地址一般是192.168.55.1,宿主机那侧是192.168.55.100,直接ssh nx-agent-01@192.168.55.1就能连上。
4.2 功耗模式、散热和长期稳定性
功耗模式决定了性能上限,也决定了散热压力。Xavier NX 提供了几个预设模式,用nvpmodel切换:
sudo nvpmodel -p --verbose # 列出所有模式 sudo nvpmodel -q # 查看当前模式 sudo nvpmodel -m 2 # 切到某个模式 sudo jetson_clocks # 把时钟锁到该模式的最高频 sudo jetson_clocks --show # 查看当前频率模式编号在不同 L4T 版本之间会有差异,所以我不在这里列一张固定的表,你自己跑一遍nvpmodel -p --verbose看输出最准。大致上是 10W(少核低频)、15W(六核中频)、20W(六核高频)这几档。跑 Agent 的时候我的建议是:常驻的小模型推理用 15W 就够,做模型转换或者批量跑测试的时候切 20W。
散热是 Xavier NX 最容易出问题的地方。开发套件自带的那颗小风扇在高负载下压不住,tegrastats里看到 GPU 温度超过 85 度就说明该处理了。我的做法是换了更大尺寸的风扇加铜片,然后把风扇控制打开:
sudo jetson_clocks --fan watch -n 1 tegrastatstegrastats的输出里重点看这几个值:GR3D_FREQ(GPU 频率,如果一直在最高频说明没降频)、tj@(结温)、RAM(内存占用)。跑 Agent 的时候我一般会挂一个后台脚本,每隔一段时间记一次温度和内存,出问题的时候有历史数据可查:
#!/bin/bash # /opt/agent/health.sh while true; do echo "$(date '+%F %T') $(tegrastats --interval 1000 --count 1)" >> /var/log/nx-health.log sleep 30 done日志文件记得做轮转,不然跑一个月能把 NVMe 写满。
4.3 CUDA、TensorRT 和 Python 生态的版本对齐
这是所有坑里最容易让人崩溃的一类:版本全对不上。Xavier NX 的 JetPack 5.x 根文件系统是 Ubuntu 20.04,系统 Python 是 3.8。你从 pip 上装的默认 PyTorch 是给 x86 或者给新版本 CUDA 编译的,装完torch.cuda.is_available()返回 False,然后你会开始怀疑驱动、怀疑刷机没刷好,其实只是 wheel 拿错了。
正确做法是用 NVIDIA 为 JetPack 提供的 aarch64 wheel:
# 以 JetPack 5.x + Python 3.8 为例,具体文件名以官方页面为准 wget https://developer.download.nvidia.com/compute/redist/jp/v512/pytorch/torch-2.1.0a0+41361538.nv23.06-cp38-cp38-linux_aarch64.whl pip3 install torch-2.1.0a0+41361538.nv23.06-cp38-cp38-linux_aarch64.whl装完立刻验证:
python3 - <<'EOF' import torch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) print("device:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "none") print("capability:", torch.cuda.get_device_capability(0) if torch.cuda.is_available() else "none") EOF最后一行应该输出(7, 2),这就是 Xavier NX 的 compute capability。如果输出是别的数字,说明 torch 认错了架构,后面跑什么都快不了。
torchvision 没有官方 wheel,得从源码编。这一步在 Xavier NX 上要花四十分钟左右,编之前先把依赖装上:
sudo apt install -y libjpeg-dev zlib1g-dev libpython3-dev libopenblas-dev \ libavcodec-dev libavformat-dev libswscale-dev libavutil-dev export BUILD_VERSION=0.16.1 git clone --branch v0.16.1 --depth 1 https://github.com/pytorch/vision torchvision cd torchvision && python3 setup.py install --user版本号要和 torch 对上,torch 2.1 对应 torchvision 0.16.x,对错了编译能过但 import 会报符号错误。
TensorRT 的用法是先把 ONNX 转成 engine,这一步必须在目标设备上做,因为 engine 是绑定具体 GPU 架构和 TensorRT 版本的:
/usr/src/tensorrt/bin/trtexec \ --onnx=yolov8n.onnx \ --saveEngine=yolov8n_fp16.engine \ --fp16 \ --workspace=2048--workspace给的是构建期的临时显存,单位 MB。Xavier NX 只有 8GB 或 16GB 统一内存,给 2048 到 4096 就够了,给太大反而会挤压别的进程。第一次转 engine 会花几分钟,转好之后每次加载都是秒级。
注意:engine 文件不要跨设备复制。就算两台机器都是 Xavier NX、都刷的 5.1.7,只要其中一个 OTA 升级过 TensorRT 的小版本,engine 就会反序列化失败。要批量部署就批量转,或者在部署脚本里加一步自动转换。
4.4 内存预算:8GB 和 16GB 差了整整一个模型
Xavier NX 有 8GB 和 16GB 两个版本,这两个版本在 Agent 场景里的能力差距不是"能跑更多东西",而是"能不能跑得动"。
16GB 版本上,我实测可以这样分配:系统本身占 1.5GB 左右,视觉模型(YOLOv8n 的 FP16 engine)占 0.5GB,一个 3B 参数的 4-bit 量化语言模型加上 4K 上下文大约占 3.5GB,剩下 10GB 左右留给 Agent 的运行时、向量库、日志和文件缓存。这个配置相当从容。
8GB 版本就必须做取舍。要么把语言模型降到 1.5B 级别,要么把视觉那一侧改成按需启停,要么放弃本地向量库改用简单的关键词索引。我手上有一台 8GB 的机器做对比测试,结论是:跑一个 1.5B 的语言模型加一路 YOLOv8n,内存占用能稳在 6GB 左右,再想加东西就要开始动 swap 了。
swap 这块有个经验:不要放在 eMMC 上。eMMC 的写入寿命有限,swap 频繁换页会加速磨损,而且速度也慢。放在 NVMe 上就好很多:
sudo fallocate -l 16G /mnt/nvme/swapfile sudo chmod 600 /mnt/nvme/swapfile sudo mkswap /mnt/nvme/swapfile sudo swapon /mnt/nvme/swapfile # 写入 /etc/fstab 保证重启后自动挂载 echo '/mnt/nvme/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab设置 swappiness 低一点,让内核只有在真的撑不住的时候才用 swap:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf另一种思路是用 zram,把一部分内存压缩后当 swap 用,好处是不碰磁盘,坏处是占 CPU。Xavier NX 的 CPU 核心够多,跑一个 zram 对整体影响不大,值得一试。
4.5 把 Agent 的运行时搭起来
环境干净之后,跑 Agent 就变成了普通的工程问题。我的做法是用容器隔离,因为依赖冲突会毁掉一整天的调试时间。
JetPack 5.x 上装 NVIDIA 容器运行时比较讲究,用jetson-containers那套工具最省心,它会根据你的 L4T 版本自动挑对的基础镜像:
git clone https://github.com/dusty-nv/jetson-containers bash jetson-containers/install.sh jetson-containers run $(autotag l4t-pytorch)语言模型这一侧,我的首选是 llama.cpp,因为它的 CUDA 后端在 Xavier 这种算力有限的设备上效率很高,编译也简单:
git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=72 cmake --build build --config Release -j6CMAKE_CUDA_ARCHITECTURES=72这个参数是关键,72 就是 compute capability 7.2,不指定的话编译器会为所有架构都生成一份代码,编译时间翻好几倍,编出来的二进制也大。
模型选型上,3B 级别的 4-bit 量化模型在 Xavier NX 上跑起来大概是每秒十来个 token 的量级,1.5B 级别能到每秒二十个左右,具体数字跟上下文长度、量化档位、是否开了 flash attention 都有关系。跑 Agent 的时候我一般把上下文压在 2048 到 4096 之间,再长的话首 token 延迟会明显上升。
起一个 OpenAI 兼容的服务端:
./build/bin/llama-server \ -m /opt/models/qwen2.5-3b-instruct-q4_k_m.gguf \ -c 4096 \ --n-gpu-layers 99 \ --host 0.0.0.0 \ --port 8080 \ --threads 6--n-gpu-layers 99表示尽可能把所有层都放到 GPU 上,模型装得下的时候这样最快。如果内存紧张会 OOM,就往下调这个数字,让一部分层跑在 CPU 上,速度会掉但至少能跑。
Agent 框架这一侧,LangChain 或者更轻的本地实现都可以,网络请求指向http://localhost:8080/v1,工具调用就用函数调用那套协议。这块内容展开就太多了,核心结论是:把推理服务做成一个稳定的本地 endpoint,上层的 Agent 逻辑怎么做都不太受影响。
5. 常见故障速查:从刷不进去到跑不动
5.1 刷机阶段的报错和处理
这部分我尽量把遇到过的都列出来,按报错现象查表就行。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
lsusb里没有 NVIDIA 设备 | 没进 recovery、线不对、USB 口问题 | 重新走按键流程,换线换口,直连主板 |
设备出现了但不是0955:7020 | 进的是普通模式或者别的设备 | 断电重来,先按 REC 再上电 |
USB write error/tegrarcm中途失败 | 数据线质量差、USB Hub 干扰 | 换短屏蔽线,去掉所有转接 |
卡在Waiting for device to boot | 分区表不匹配、显存/内存镜像损坏 | 检查板级配置名,重新生成镜像 |
刷 NVMe 时报找不到nvme0n1p1 | SSD 没插好、分区编号不对 | 把 SSD 插到宿主机上确认编号 |
| 提示空间不足 | eMMC 容量小于镜像 | 用-S收紧根分区,或者改刷 NVMe |
脚本报flash_l4t_external.xml找不到 | 工作目录不对 | 必须在Linux_for_Tegra下执行 |
| QSPI 更新阶段报校验失败 | 引导固件和板型编号不匹配 | 用-p显式指定正确的 xml |
有一个特别隐蔽的问题值得单独说:如果你用的是第三方载板,板级配置名可能得用自定义的.conf,或者干脆用jetson-xavier-nx-devkit-emmc加一堆覆盖参数。这种情况下设备树很可能不完整,表现为刷机成功、启动也有日志,但网卡或者某个 USB 口不工作。遇到这种情况先看串口日志里有没有设备树相关的告警,再去跟载板厂商要适配文件。
5.2 首次启动和后续装机的典型问题
刷完之后的问题大多和存储、版本、网络有关,我整理成一张速查表:
| 现象 | 排查方向 | 处理方式 |
|---|---|---|
| HDMI 无输出但串口正常 | 显示配置、分辨率协商 | 用 SSH 登录,检查xrandr和内核显示相关日志 |
卡在Waiting for root device | 根分区不在预期设备上 | 串口看 U-Boot 的引导参数,确认 NVMe 是否被识别 |
| apt 报证书时间错误 | 系统时钟漂了 | 装 chrony 强制同步 |
torch.cuda.is_available()返回 False | wheel 架构或版本不对 | 换 JetPack 对应的 aarch64 wheel |
| import torchvision 报符号错误 | torch 和 torchvision 版本不匹配 | 按对应关系重编 |
| TensorRT 加载 engine 失败 | engine 和当前 TRT 版本不匹配 | 在目标设备上重新转 |
| pip 装包报编译错误 | 缺系统依赖 | 先装python3-dev和对应的libxxx-dev |
| USB 网络共享连不上 | 宿主机防火墙拦了虚拟网卡 | 放行192.168.55.0/24 |
5.3 跑起来之后才暴露的性能问题
这类问题不报错,只是慢,排查起来反而更费时间。几个我踩过的:
GPU 频率上不去。明明负载很重,tegrastats里GR3D_FREQ却只有一半。原因通常是没切功耗模式,系统还在默认的低功耗档。nvpmodel -q看一眼当前模式,需要的话切到 15W 或 20W,再跑jetson_clocks。
推理速度比预期慢一截。先确认模型真的在 GPU 上。llama.cpp 的启动日志里会打印每一层被分配到哪个设备,如果看到大量CPU,说明--n-gpu-layers给小了或者显存不够被挤下去了。TensorRT 这边用trtexec的 profile 输出,看是哪个算子拖后腿。
跑一段时间变慢。基本都是温度墙。用手摸散热片都觉得烫的话,tegrastats里的结温肯定过 85 了。硬件上加散热,软件上把功耗模式降一档,或者给推理任务做个限流。
IO 等待把 CPU 吃满。如果你的模型文件、swap、日志全在一个慢设备上,iostat里能看到明显的等待。把模型放 NVMe、日志做轮转、swap 限制使用频率,这三个动作做完一般就好了。
6. 我踩过之后才明白的几个细节
6.1 刷机前一定要能回滚
重刷之前,先把当前系统完整备份一份,不然万一新系统有问题,你连对比的基准都没有。eMMC 上的系统可以整个克隆出来:
sudo ./flash.sh -r -k APP -G backup-nx-$(date +%Y%m%d).img \ jetson-xavier-nx-devkit-emmc mmcblk0p1这条命令会把 APP 分区读出来存成一个镜像文件,需要的时候可以用-r加这个镜像刷回去。文件可能有十几 G,宿主机上留够空间。
另外,把/boot/extlinux/extlinux.conf、自定义的设备树、/etc/fstab、以及你装过的所有 pip 包清单(pip3 freeze)导出来存好。这几份文件加起来几十 KB,但它们能让你的恢复时间从几小时压到十几分钟。
6.2 版本冻结比"保持最新"更重要
做边缘设备最容易犯的错误,是拿对待笔记本的习惯对待它——看到有更新就升级。Jetson 这套东西的版本耦合非常紧:L4T 版本、内核版本、CUDA、cuDNN、TensorRT、PyTorch wheel、你编译的 engine 文件,这条链上任何一环升级了,后面的都要重新验证。
我的做法是:设备跑通并且验证完成后,把整套版本号记进一个VERSIONS.md,然后关掉自动更新,只在需要的时候手动升级并且重新走一遍验证。这个习惯听起来保守,但它能把"上周还好好的今天怎么不行了"这类问题出现的频率降低一个数量级。
具体的版本锁定方式:
# 记录当前版本 { cat /etc/nv_tegra_release dpkg -l | grep -E "cuda|tensorrt|cudnn|opencv|nvidia-jetpack" python3 -c "import torch; print(torch.__version__)" /usr/src/tensorrt/bin/trtexec --version 2>/dev/null | head -3 pip3 freeze } > /opt/agent/VERSIONS.md6.3 把刷机流程脚本化
最后分享一个让我省了很多时间的做法:把整个刷机流程写成一个脚本,从进入 recovery 模式检测到最终验证,每一步都有检查和日志。
#!/bin/bash # flash-nx.sh —— 简化版,实际使用需要加错误处理和日志 set -euo pipefail L4T_DIR=~/jetson/nx-5.1.7/Linux_for_Tegra BOARD=jetson-xavier-nx-devkit-emmc echo "[1/4] 等待设备进入 recovery 模式..." until lsusb | grep -q "0955:7020"; do sleep 2 done echo "设备已就绪" echo "[2/4] 开始刷写..." cd "$L4T_DIR" sudo ./tools/kernel_flash/l4t_initrd_flash.sh \ --external-device nvme0n1p1 \ -c tools/kernel_flash/flash_l4t_external.xml \ -p "-c bootloader/t186ref/cfg/flash_l4t_t194_qspi_p3668.xml" \ --showlogs --network usb0 "$BOARD" internal echo "[3/4] 等待设备重启..." sleep 60 echo "[4/4] 验证..." ssh nx-agent-01@192.168.55.1 'cat /etc/nv_tegra_release && df -h /'这个脚本最大的价值不在于省了敲命令的时间,而在于它强制你在一开始就把"什么算刷成功"定义清楚。我最早的版本没有第 4 步验证,结果有两次刷完之后发现是旧系统还在跑,白白浪费了一轮调试。
写脚本的时候有个细节要注意:set -e配合until循环的时候,如果循环体里某条命令返回非零,脚本会直接退出。上面那个grep -q在没匹配到的时候返回 1,所以循环外面用了until而不是while,语义上是等它出现。这类小坑不写脚本的时候根本不会遇到,写了就会。
再补一个实用的小技巧:给每台设备在机身上贴一张标签,写上主机名、刷机日期、JetPack 版本、NVMe 型号。我手上设备一多之后,纯靠记忆已经完全对不上了,logo 上那张手写标签救过我好几次。设备是拿来跑活的,不是拿来当收藏品的,让每一台的状态可追溯,比什么都重要。