做嵌入式或者CPU方向的朋友,应该都有过这种经历:芯片还在设计验证阶段,软件却已经要动工了。最近我在折腾手头一个AI算法往RISC-V架构上迁移的活儿,手边没有对应开发板,也不可能每一代芯片都买一块回来等着,最后落地的方案就是用QEMU拉起一块RISC-V AI芯片的“虚拟实验台”,再配合现在很流行的AI Agent来加速环境搭建和问题排查。这篇就把我整个搭建过程、踩过的坑、还有哪些地方值得自动化,完整梳理一遍。不管你是做编译器移植、内核验证,还是想在RISC-V上跑PyTorch算子验证,这套流程都能帮你省掉不少弯路。
1. 为什么要把RISC-V AI芯片实验台搬进虚拟机
1.1 QEMU到底在模拟什么
先明确一个概念:QEMU是一个纯软件模拟器,它模拟的不是某个“操作系统”,而是完整的一台机器——CPU指令集、内存控制器、中断控制器、UART串口、virtio磁盘和网络设备,全部用软件翻译的方式跑在宿主机上。你可以把它理解成一台“没有实体电路但能通电的虚拟开发板”,照着芯片的指令集手册和数据手册,在软件里实现了一个行为等价的硬件平台。
对于AI芯片实验来说,这一点格外重要。很多AI加速器芯片内部其实是“RISC-V核心+专用NPU/向量单元”的组合,NPU部分QEMU通常不会精确模拟,但RISC-V主控CPU部分、内存子系统、外设总线、中断控制器这些都是标准化的,QEMU的virt机器类型支持得很完整。我这次的目标,是把AI算法里的向量算子、矩阵运算库、调度逻辑在RISC-V指令集上先验证跑通,而不是去做性能摸底——性能摸底那是以后真芯片回来之后的事。
1.2 为什么偏选RISC-V做AI芯片实验
AI芯片选RISC-V作为主控或者协处理核心,在行业里已经很常见了。原因不难理解:RISC-V指令集开放、模块化,厂商可以根据AI负载自由加减指令扩展。尤其是向量扩展(RVV),一套寄存器可以同时操作多条数据,做矩阵乘法、卷积这类算子时,代码密度和理论上限都很合适。
但开放指令集带来的另一个问题就是:生态碎片明显。不同厂商的SoC可能选了不同的扩展组合,有的带V扩展,有的只带M扩展,缺了某个扩展代码怎么优雅降级,这些都需要提前验证。QEMU的好处是它允许我在命令行里直接指定CPU特性,比如打开v=true启用向量扩展,然后用一套软件栈测试不同配置下的兼容性。这在真实板卡上反而很难做到,因为板卡CPU特性出厂就定死了。你可以把QEMU当成一枚“可以随时改芯片配置的软芯片”,这一特性在做AI算子移植时特别顺手。
1.3 AGENT在这里扮演什么角色
环境搭建这个事表面看只是几条命令,实际做起来要反复处理依赖缺失、启动参数写错、内核挂载不了根文件系统之类的问题。每报一次错,就得去查文档、看日志、改参数、重跑。这些操作重复度高、规律性强,非常适合交给AI Agent去辅助。
我这里的Agent,指的是现在常见的命令行智能体、编程助手或者自动化任务执行器,它能理解自然语言描述、生成命令、执行命令、读取回显,再根据错误信息自我修正。你可以把它想象成一个“能读日志、能改脚本、能反复执行命令的实习生”:有明确任务边界的时候,比人工手动试错快得多。但注意,它不是万能的,关键决策还是要人来把控——毕竟它不懂你的项目背景,也不理解哪些内核参数动了会产生潜在风险。所以我给它设置的工作范围,就限定在“为这个RISC-V虚拟机生成启动脚本、解析启动日志、检查依赖包”这几件事上。
2. 搭建前先盘点:宿主机与组件选型
2.1 宿主机准备与依赖安装
QEMU本身是跨平台的,Linux、macOS、Windows都有对应的包。我这次用的是Ubuntu系的Linux宿主机,原因比较实在:RISC-V相关工具链和交叉编译环境在APT源里基本都有,省去很多手动编译时间。
先确认一下宿主机有没有装QEMU的RISC-V系统模拟器。不同发行版包名有差异,Ubuntu/Debian下,包含qemu-system-riscv64的包是qemu-system-misc,另外顺手装一个qemu-user-binfmt,这个包用于用户态模拟,后面跑RISC-V交叉编译的可执行文件时会用到。
sudo apt update sudo apt install qemu-system-misc qemu-user-binfmt qemu-system-riscv64 --version版本信息能正常打印出来就可以继续了。这里多说一句,很多人分不清qemu-system-riscv64和qemu-riscv64的区别:前者模拟完整机器,可以启动内核;后者只模拟用户态,只能跑单个可执行文件。这次搭建实验台,我们需要的是前者。
2.2 三种镜像路线怎么选
启动一台RISC-V虚拟机,本质上需要三样东西:固件、内核、根文件系统。固件通常用OpenSBI,负责机器模式(M-mode)的初始化;内核用编译好的Linux RISC-V内核镜像;根文件系统用Buildroot生成的rootfs.ext4或者发行版预制的rootfs.img。
新手第一反应可能是“我自己用Buildroot编一个”,但我建议第一次先别急着自己编。Buildroot全量编译RISC-V工具链加内核,在普通机器上要跑一两个小时,期间还会遇到各种网络和版本问题。更稳妥的路线是先下载别人打包好的预制镜像,把虚拟机跑起来,熟悉启动流程之后,再回头自己编内核和rootfs。我实际用的就是Fedora/RISC-V的预制镜像加OpenSBI固件,两个文件加起来也就几百MB,下载路径很容易找到。第二个选择是Ubuntu的RISC-V版,起步更顺,包管理器成熟,后期装Python、PyTorch都比Buildroot的busybox环境方便得多。
2.3 搞清楚各组件版本的匹配关系
环境搭建里最隐蔽的坑,其实是版本匹配。QEMU、OpenSBI、Linux内核这三个项目都在快速迭代,不是随便拿一个OpenSBI配一个内核就能跑起来。我遇到过一次内核启动到一半卡在init阶段,最后换了一个更新版本的OpenSBI固件就正常了。原因是旧固件没有正确配置好某个CSR(控制状态寄存器),导致内核访问到预期之外的值。
所以建议你在搭建的时候记录三样东西:QEMU版本号、OpenSBI版本、内核版本。低于2023年版本的QEMU,对部分RISC-V扩展支持不完整,如果你的目标是AI向量算子实验,尽量用较新的QEMU版本,确保支持RVV 1.0规范。这里可以用一张小表把方案定下来:
| 组件 | 我的选型 | 说明 |
|---|---|---|
| 宿主机 | Ubuntu 22.04 / 24.04 | 包齐全,社区资料多 |
| QEMU | 8.0以上 | 对RVV和virt设备支持较好 |
| 固件 | OpenSBI较新release | 避免旧固件启动内核失败 |
| 内核 | 6.x内核 | RISC-V支持成熟 |
| 根文件系统 | Fedora/RISC-V预制镜像 | 方便,包管理器顺手 |
这个组合本身不是唯一解,但它是兼容性风险最低、参考资料最多的组合。
3. 从零拉起RISC-V虚拟机的完整实操
3.1 最小启动命令逐段拆解
先把最小可用的启动命令写出来,这条命令我会一直用,后面所有排错都从它开始。命令看起来很绕,实际每个参数都有明确作用:
qemu-system-riscv64 \ -machine virt \ -cpu rv64,v=true \ -smp 4 \ -m 8G \ -bios default \ -kernel Image \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "root=/dev/vda rw console=ttyS0" \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-device,netdev=net0 \ -nographic逐段解释一下:
-machine virt:指定QEMU模拟的机器类型。RISC-V QEMU里最常用的是virt,它统一定义了中断控制器、UART、PCIe总线,类似ARM平台的virt机型,兼容性最好。-cpu rv64,v=true:指定CPU架构为RV64,并开启向量扩展。这个v=true对AI实验是关键参数,后面跑向量算子就靠它。-smp 4 -m 8G:4核CPU,8G内存。RISC-V TCG模式是纯软件模拟,核数和内存不要盲目开高,否则宿主机容易卡死,8G内存对大多数算子验证已经够用。-bios default:使用QEMU内置的OpenSBI固件,自动完成M-mode初始化。-kernel Image:指定Linux内核镜像路径。-drive file=rootfs.img,format=raw,id=hd0:挂载rootfs作为磁盘,format=raw直接标注为裸镜像,避免QEMU去探测格式导致歧义。-device virtio-blk-device,drive=hd0:把磁盘挂到virtio-blk总线上,速度快,驱动支持好。-append "root=/dev/vda rw console=ttyS0":把内核启动参数传过去,告诉内核根设备是/dev/vda,控制台用串口。-netdev user,id=net0,hostfwd=tcp::2222-:22:配置用户模式网络,并把宿主机的2222端口转发到虚拟机里的22端口,后续SSH登录用。-nographic:不用图形窗口,串口直接映射到当前终端。
3.2 启动日志应该长什么样
执行上面命令后,终端会陆续打印两类日志。先是OpenSBI的banner,包含版本号和平台信息;
然后进入Linux内核启动日志,出现[ 0.000000] Linux version 6.x.0 ...这样的内核版本行,再往下可以看到内存布局、设备树、SMP CPU核数等初始化信息。
启动到最后的标志性输出是:
Welcome to Fedora RISC-V ... [ OK ] Reached target Multi-User System.看到login提示符,说明整个虚拟开发板已经正常起来了。如果卡在前面某一步,后面会让你头疼,但大部分问题都能从日志里定位。
3.3 网络和SSH登录配置
虚拟机起来了之后,直接面对黑黢黢的串口终端操作并不舒服。我的建议是顺手把SSH配好,之后就可以像连普通服务器一样操作。
虚拟机里面先做这几步:
# 查看网卡是否已经获取到地址 ip addr # 如果没有地址,使用DHCP客户端获取 sudo dhclient eth0 # 安装并启动SSH服务 sudo dnf install -y openssh-server sudo systemctl enable --now sshd随后在宿主机上,因为启动命令里已经有hostfwd=tcp::2222-:22,直接:
ssh -p 2222 riscv@localhost这就是QEMU用户模式网络最方便的地方,不需要配置复杂的网络桥接,就能从宿主机访问虚拟机内部服务。网络不通时,优先检查-netdev参数有没有写错,以及在虚拟机里ip addr是否看到eth0。
3.4 为AI实验额外预留的几个QEMU参数
上面那条是最小启动配置。一旦你确定AI实验方向,还可以加上这些参数提升体验:
-rtc base=utc \ -no-reboot \ -fsdev local,security_mode=none,id=fsdev0,path=/host_share \ -device virtio-9p-device,fsdev=fsdev0,mount_tag=hostshare-no-reboot是排查问题时特别好用的参数,内核panic后QEMU不会自动重启,而是停在崩溃现场等你观察。-fsdev配置9p共享目录,把宿主机的一个目录直接共享进虚拟机,做代码同步和日志提取非常方便。挂载方式是在虚拟机内部执行:
sudo mount -t 9p -o trans=virtio hostshare /mnt这样你在宿主上编辑代码,虚拟机里直接从/mnt读取,不需要来回scp。我后续所有算子实验都是在这种共享目录下完成的。
4. 用AGENT把环境搭建串成自动化流程
4.1 环境搭建为什么适合交给Agent
环境搭建过程里最多的时间不是花在敲命令上,而是花在“试错-看日志-改参数-再试”这个循环里。每一次报错,人工处理需要理解错误信息、定位组件、查资料、修正命令,但这类问题的解决模式高度重复,AI Agent恰恰擅长这种模式。
举个实际例子。我最初写启动命令时,把内核启动参数写成root=/dev/sda,结果启动后一直报VFS: Cannot open root device "sda"。我让Agent基于日志排查,它很快指出virtio-blk设备在RISC-V virt平台上通常注册为/dev/vda,改掉之后系统直接起来了。这个判断本身不复杂,但省掉了读文档和试错的时间。
更复杂的场景是内核模块依赖。如果你之后要加一个特定的virtio驱动模块,Agent可以从内核源码和Kconfig里把依赖关系梳理出来,生成一份补丁式的配置说明。这些都属于“信息检索+命令执行”的强项。
4.2 一个可复用的Agent工作流设计
为了不把Agent用成“一次性问答”,我有意把整个环境搭建过程拆成了几个固定阶段,每个阶段都有明确的输入输出:
- 目标定义:告诉Agent最终要得到什么。比如“在QEMU virt平台上启动Fedora RISC-V,内核带RVV支持”。
- 环境探测:Agent先在宿主机执行
uname -m、qemu-system-riscv64 --version等命令,确认环境基础。 - 脚本生成:Agent根据探测结果生成启动脚本,脚本里带参数检查和日志输出重定向,避免跑完命令不知道发生了什么。
- 执行与验证:Agent执行启动脚本,比对输出中的关键标志(
Booting Linux、Reached target)。 - 错误迭代:如果失败,Agent抓取日志最后100行,提炼可能的根因,修改参数后重试,最多尝试5次就停下来请示。
这个流程你可以直接用各种命令行智能体工具的主流程来套,核心思想是每步都有可验证的输出物,而不是黑盒跑一条命令就结束。
4.3 幂等性:反复跑脚本不搞出脏状态
环境搭建脚本最忌讳的是“跑一次能用,跑第二次就坏”。我在设计启动脚本时引入了状态管理逻辑,用一个STATE_DIR目录记录下载是否完成,用锁文件避免并发冲突。简化版如下:
#!/usr/bin/env bash STATE_DIR=${STATE_DIR:-/srv/riscv_lab/state} mkdir -p "$STATE_DIR" if [ ! -f "$STATE_DIR/Image.done" ]; then echo "下载内核镜像..." # 下载完成后创建标记文件 touch "$STATE_DIR/Image.done" fi if [ ! -f "$STATE_DIR/rootfs.done" ]; then echo "下载rootfs..." touch "$STATE_DIR/rootfs.done" fi exec qemu-system-riscv64 \ -machine virt \ -cpu rv64,v=true \ -smp 4 \ -m 8G \ -bios default \ -kernel "$STATE_DIR/Image" \ ...这里的细节是:用“标记文件”而不用“文件是否存在”来判断,因为镜像文件可能下载了一半。标记文件的存在意味着这个文件已经完整落盘,不会每次重跑都重新下载。这个习惯是从构建系统里学来的,用在环境搭建脚本里同样顺手。
4.4 Agent操作的安全边界不能省
让Agent主动执行命令,安全边界一定要提前设好。我的习惯是三条硬性约束:
- 只允许Agent在当前项目目录和
STATE_DIR内写文件,不允许读取或修改系统级目录。 - 所有需要
sudo的命令必须经过我确认。 - 不运行任何从不明来源自动下载的二进制脚本,下载动作只允许通过命令行包管理器或明确的官方链接。
这些约束看起来繁琐,但在自动化流程中能避免很多意外。毕竟Agent的“意图理解”能力再强,也无法替你判断一个未知脚本的副作用。你自己都不确定的东西,别让它替你做决定。
5. 进入系统后的AI环境配置:从Python到PyTorch
5.1 在RISC-V用户空间里装Python和PyTorch
虚拟机起来之后,最关心的就是AI软件栈能不能跑。在Fedora RISC-V镜像里,Python 3环境是预装好的:
python3 --version pip3 --version如果还没装,用包管理器安装即可。这里要注意一点,RISC-V架构的pip源里不是所有包都有预编译wheel,安装PyTorch时最稳妥的方式是检查是否提供了riscv64的wheel包,或者用pip直接安装源码包,让它在本地编译。实测下来,源码编译PyTorch在QEMU环境下耗时非常长,8G内存都显得紧张,所以我更推荐先用现成的rpm包,把环境跑通,再考虑针对特定算子做源码级编译优化。
一个比较轻量的落地做法:先在虚拟机里创建一个虚拟环境,然后用PyTorch的CPU版本做矩阵运算验证。因为QEMU模拟的是纯CPU环境,GPU相关的CUDA逻辑用不上,所以CPU版是更合适的选择。
5.2 跑一个最小算子验证
进入Python环境后,我通常会跑一个最朴素的矩阵乘法脚本,先验证PyTorch的RISC-V后端是否工作正常:
import torch x = torch.randn(32, 32, dtype=torch.float32) y = torch.randn(32, 32, dtype=torch.float32) z = torch.mm(x, y) print(z.shape) print(z[0, :3])输出能打印出torch.Size([32, 32])和数值,说明PyTorch在RISC-V虚拟机上已经完成了最基本的张量运算链路。接着可以试试更接近AI负载的操作,比如对某个张量做卷积、softmax、归一化:
import torch.nn.functional as F input_t = torch.randn(1, 8, 16, 16) weight_t = torch.randn(4, 8, 3, 3) out = F.conv2d(input_t, weight_t, padding=1) print(out.shape)这类算子跑起来很慢,但能验证算子实现的正确性,对算法迁移前排查逻辑问题已经足够。就像用模拟器练手,你关心的是能不能跑对,不指望它飞快。
5.3 向量扩展能给AI算子带来什么
前面启动时给CPU加了v=true,这是为后续做向量算子实验预留的。RISC-V的V扩展允许一条指令同时处理多个数据元素,类似SIMD,AI算子里的逐元素运算(activation、normalization)和矩阵分块运算都可以受益。
验证向量扩展是否生效,可以从/proc/cpuinfo看到标志位:
cat /proc/cpuinfo | grep isa # 如果看到 rv64imafdcv 这样的字符串,说明V扩展已经在当前核上开启了字符串末尾的v就是向量扩展标记。有了它,后续无论是自己写汇编测试向量指令,还是编译带auto-vectorization的C代码,都有基础支撑。不过要说明的是,QEMU的向量性能不能代表真实芯片,它的意义在于让你提前适配指令行为和编译选项。
5.4 与真实开发板的差异要心里有数
用QEMU做AI实验,本质上是在“指令行为一致,时序性忽略不计”的前提下运作。以下几点差异,建议早点认清:
- 性能:QEMU TCG模式是动态二进制翻译,比真实硬件慢几个数量级,矩阵比较大的训练任务不要指望现场出结果。
- 时序:外设的中断时序、DMA行为都只是近似,涉及实时性的代码不适合在这里验证。
- NPU:QEMU一般不模拟专用NPU指令,AI加速器厂商的私有指令集需要真实芯片或厂商自研模拟器。
- 功耗:没有物理芯片,功耗评估为零。
但它也有真实开发板比不了的价值:虚拟硬件配置可以在命令行里随意改动,同一个测试可以把CPU核数、内存大小、扩展开关都切换一遍,这对软件栈的健壮性验证比收几块固定配置的开发板高效得多。CI系统里也可以天天跑RISC-V回归测试,成本几乎为零。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
这一路踩过不少坑,整理成表格,方便你对症处理:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动无任何输出 | 缺少-nographic或console=ttyS0 | 加上-nographic,内核参数里加console=ttyS0 |
卡在Starting kernel ... | OpenSBI与内核版本不匹配 | 升级OpenSBI或换新版本QEMU |
报错VFS: Cannot open root device | 根设备名写错 | 核实virtio磁盘注册名,一般是/dev/vda |
rootfs挂载成功但init崩溃 | rootfs架构不对或格式错误 | 确认rootfs是riscv64架构,格式为raw |
| SSH连接不通 | hostfwd端口冲突或虚拟机没装sshd | 改宿主端口,确认虚拟机内sshd状态 |
| 网络通了但DNS解析失败 | user模式网络的DNS配置问题 | 在虚拟机里手动配/etc/resolv.conf |
| 编译Python/PyTorch时OOM | 内存分配过小 | 提升-m,或者关闭部分并行编译任务 |
| 启动脚本第二次跑异常 | 有残留镜像或端口占用 | 清理STATE_DIR,检查2222端口占用 |
6.2 帮你省时间的几个排查技巧
第一个技巧是日志分级。启动失败时,别直接看完整串口输出,先抓最后100行,里面通常藏着真正的报错。系统起来之后,journalctl -b能看内核和服务的详细日志,定位问题比盲目改参数快得多。
第二个技巧是给QEMU加-no-reboot。内核一旦panic,默认行为是重启然后继续panic,信息一闪而过。关掉自动重启后,崩溃现场就停在终端上,可以慢慢分析。
第三个技巧是用9p共享目录做日志中转。虚拟机的日志写到共享目录,宿主机直接查看,两边都不用搭额外服务。调试SSH这类涉及网络的问题尤其好用。
第四个技巧是不要一次开满资源。如果你用-m 16G -smp 8启动,TCG模式下CPU占用会直接拉满,宿主机其他工作别想做了。先开-smp 4 -m 4G把流程跑通,再根据实际需要加资源。
6.3 关于Agent辅助排错的边界
Agent在处理这些常见问题时确实高效,但我发现它在两类场景会翻车:一是需要结合项目历史的特殊取舍时,它可能给出标准答案但不适配你的环境;二是它容易“自信地”给出一个看似合理但实际没人验证过的参数组合。所以凡是Agent给出的关键参数(特别是内核启动参数、设备树相关配置),我都会自己查一遍手册再采用。
这也回到前面说的:Agent做助理,核心决策还是自己做。把它定位成一个随叫随到、检索能力强的技术助手,环境搭建效率能提升不少,但别把整台虚拟机的命运完全交给它做主。
6.4 一个小技巧:把启动参数收拢成配置文件
最后分享一个我实际用得很舒服的习惯。不要每次手动敲一长串QEMU参数,而是写一个lab.env文件,把CPU核数、内存大小、端口转发、内核路径这些全部做成变量,启动脚本只负责读取变量并拼装命令。以后想调整配置,改一行文件即可,不用动脚本逻辑,也不会在多次实验之间留下“上一次乱改的参数”。
这样一来,整套环境就是一个可以随时重建、记录清晰、自动化友好的实验台。后续不管是接CI回归,还是换一套rootfs测试不同发行版,改动成本都非常小。AI实验台本来就是用来反复折腾的,底子打顺了,后面每一步都省心。