Kimi AgentENV:大规模 Agent 训练的执行环境
2026 年 7 月,月之暗面、清华 MADSys 和 KVCache.ai 开源了 AgentENV。Kimi K3 的 Agent 后训练就跑在它上面:每条训练轨迹分到一台 Firecracker 微虚拟机,有自己的内核和磁盘;模型思考时虚拟机可以暂停,把内存还给宿主机;做到一半的状态还能当场分叉出最多 16 份,分别往下试。
仓库在 kvcache-ai/AgentENV,介绍文章是 AgentENV 开源介绍,文档在 kvcache-ai.github.io/AgentENV。Kimi 在 K3 开放日 上把它和 MoonEP、FlashKDA 一起列为训练基础设施。它对外提供兼容 E2B 的 HTTP API,现有的 E2B SDK 改个地址就能用。
读完代码会发现,AgentENV 真正的重心在存储上。虚拟机的根文件系统和内存快照,都被做成了一叠只读层加一层可写层,格式是 overlaybd;这些层再经过 Linux 的 ublk 变成块设备,交给 Firecracker。启动快、暂停便宜、分叉几乎不花钱、镜像总量可以超过单机磁盘,都是从这一套设计里来的。所以这篇文章先讲整体架构,然后把大部分篇幅留给 overlaybd 和 ublk。
Agent 训练需要什么样的环境
一次 Agent 尝试需要一台真正能改文件的电脑:模型要读代码、装依赖、起服务、跑测试,下一条命令依赖上一条留下的文件。这台电脑必须隔离得足够好,否则模型会去翻不该看的日志,或者到外网把答案取回来。
规模上的要求更苛刻。训练同时跑着几万条轨迹,环境种类成百上千,镜像总量可以超过任何一台机器的磁盘。大部分时间虚拟机都在等模型生成下一句话,CPU 是闲的,但内存和改过的磁盘还占着。树搜索和多轨迹采样还要求从同一个中间状态再开出几条路。
把这些要求翻译成系统设计,就是四件事:
- 隔离:每个环境一台微虚拟机,一个独立内核。
- 启动快、镜像多:不能每次都把整个镜像拉下来解压,最好用到哪块再取哪块。
- 空闲便宜:暂停时只保存改过的东西,恢复时不把整份内存读回来。
- 分叉便宜:子环境和父环境共享一切没改过的数据,谁改了谁才复制。
后三件说的其实是同一个问题:大量虚拟机怎样共享同一份磁盘和内存数据,又各自保留自己改过的那一小部分。AgentENV 对这个问题的回答是分层块设备。
整体架构
AgentENV 分成三部分:多机部署时用的控制面,每台宿主机上的节点进程,以及节点旁边专门管块设备的 ublk 守护进程。
黑线是控制路径,蓝线是数据路径。节点进程只管生命周期,块设备全部交给 uvm-ublk-daemon;虚拟机看到的是普通的 virtio-blk 盘和一段文件映射的内存,背后都是 overlaybd 层。两台从同一快照恢复的虚拟机共用一个只读内存设备。
控制面:Gateway 和 Scheduler
控制面是 services/ 目录下的两个 Go 进程,和节点分开编译。单机部署时可以不要它们,直接对节点发请求。
Gateway(默认端口 8080)是 HTTP 反向代理,也是所有请求的入口。创建沙箱时,它通过 gRPC 调用 Scheduler 的 Schedule 选一台节点,成功后用 RecordAssignment 记下“这个沙箱在哪台节点上”。之后的暂停、查询等控制请求,按 URL 里的沙箱 ID 查这张表转发。访问沙箱内部服务的流量则靠请求头 x-agentenv-sandbox-id(或 e2b-sandbox-id)路由,也可以用 {端口}-{沙箱 ID}.域名 这样的主机名。
Scheduler(默认端口 9090)负责放置和节点发现。放置策略很朴素,就是轮询或随机,不考虑“哪台机器上已经缓存了这份镜像”。节点名单可以静态配置,也可以监视 Kubernetes 里 headless Service 的 EndpointSlice。每台节点定期发心跳,带上资源用量、正在跑的沙箱名单和可选的 P2P 地址;Scheduler 以心跳里的名单为准,心跳里消失的沙箱,绑定也随之删除。
这张“沙箱 → 节点”的绑定表只存在 Scheduler 的内存里。Scheduler 重启后,要等新的创建请求和下一轮心跳才能把表补齐,这期间已经在跑的沙箱暂时无法路由。文档把它列为当前的已知限制。
节点:一个 Rust 进程管一台机器
每台宿主机上跑一个 Rust 写的节点进程(入口 src/bin/server.rs),需要 /dev/kvm。在 Kubernetes 上,它以特权 DaemonSet 的方式部署,每台宿主机一个 Pod,这样才能用 KVM、网络命名空间和本机缓存。进程内部按职责分成几个子系统:
| 子系统 | 代码位置 | 职责 |
|---|---|---|
| API | src/api/ | Axum 实现的 E2B 兼容 HTTP 接口,以及进沙箱的反向代理 |
| Orchestrator | src/orchestrator/ | 沙箱状态机、超时回收、重启后恢复暂停中的沙箱 |
| Firecracker 运行时 | src/sandbox/firecracker/ | 拉起虚拟机、挂磁盘和网络、与客户机里的 envd 通信 |
| 存储集成 | src/sandbox/ublk/ | 通过 ublk 守护进程创建、快照、释放块设备 |
| 快照管理 | src/snapshot/ | 把沙箱状态提交成可再次启动的快照,存到本机、对象存储或共享文件系统 |
| 模板构建 | src/template/ | 在临时沙箱里执行构建步骤,把结果收成模板 |
| P2P | src/p2p/ | 可选,节点之间按内容互相拉取层文件 |
Orchestrator 维护每个沙箱的状态:创建中、运行中、暂停中、已暂停,以及做快照和分叉时的中间状态,最后是销毁。过期的沙箱由后台大约每秒扫描一次回收。暂停中的沙箱记录在本机目录里,节点重启后会以暂停状态重新装回。如果开了自动恢复,请求打到一台已暂停的沙箱时会先把它拉起来,并把剩余存活时间至少补到配置的下限(默认 300 秒)。
为了让恢复更快,节点准备了两级预热池。[pool.firecracker] 预先准备好“网络槽 + 已经启动的 Firecracker 进程”,恢复时直接接手,省掉 fork 进程和等待 API 套接字的时间。[pool.block] 是 ublk 守护进程里的块设备池,后面讲 ublk 时再展开。
客户机里:两块盘和一个 envd
每台微虚拟机至少挂两块盘:
/dev/vda是工具盘(tools drive),一个只读的 ext4 镜像,里面是 init 脚本、envd 和一些工具。它按版本号固定,所有沙箱共用,快照会记下自己用的是哪个版本。
/dev/vdb是用户镜像,可写,背后就是一个 overlaybd 设备。
内核从工具盘启动,init 脚本先挂上 /dev/vdb,把工具盘 bind mount 到用户文件系统的 /agentenv 下,然后 pivot_root 切进用户文件系统。这样用户镜像里不需要预装任何 AgentENV 的东西,一个普通的 OCI 镜像就能当环境用。
在客户机里接收命令的是 envd(thirdparty/envd/),沿用 E2B 的实现:执行命令、读写文件、管理进程、报告健康状态。宿主机通过 Firecracker 的 MMDS 在 169.254.169.254 上告诉客户机它的沙箱身份和快照身份。
overlaybd:块级别的分层镜像
容器镜像是分层的:基础系统一层,装依赖一层,拷代码一层。Docker 用 overlayfs 把这些层叠成一棵目录树,这是文件级的分层,上层通过同名文件或“白化”标记遮住下层。
微虚拟机用不了这一套。Firecracker 只提供 virtio-blk 块设备,没有 virtio-fs,客户机需要的是一块“盘”,而不是一棵目录树。overlaybd 换了一个角度:在块这一层做分层。每一层不再记录“哪些文件变了”,而是记录“这块虚拟盘上哪些扇区变了,新内容是什么”。把所有层按顺序叠起来,就得到一块完整的虚拟盘,上面是一个普通的 ext4 文件系统。
这个设计来自阿里巴巴的 DADI,发表在 USENIX ATC 2020(DADI: Block-Level Image Service for Agile and Elastic Application Deployment),后来以 overlaybd 的名字开源,成为 containerd 的一种远程镜像格式。AgentENV 在 storage/overlaybd/ 里用 Rust 重新实现了它的层格式和读写路径。
块级分层有几个文件级分层没有的好处:
- 改一个字节只复制一个块。overlayfs 改一个大文件的一个字节,要先把整个文件复制到上层(copy-up);overlaybd 只在上层追加被改的那几个扇区。
- 和文件系统无关。层里存的是扇区,客户机里跑 ext4 还是别的文件系统都行,宿主机不需要理解它。
- 天然支持按需读取。一块盘的读取就是“读第几号扇区”,很容易映射成“去远端取这一层文件的某个字节范围”,不需要先把整层下载下来解压。
- 快照就是封存一层。把当前可写层封存成只读层,再开一个新的空可写层,快照就做完了,不用复制任何数据。
层文件长什么样
一个 overlaybd 层文件由三部分组成:数据区、索引和头尾。
数据区只追加、不覆盖;“这段虚拟扇区现在是什么”全由索引决定。所以封存一层、叠一层、按偏移去远端取一段数据,都只是改索引或查索引。
数据区按追加顺序存放这一层写入的扇区内容。索引是一组段映射(segment mapping),每条 16 字节,记录一段连续扇区的映射关系:
- 虚拟偏移(50 位):这段数据在虚拟盘上从第几个扇区开始,单位是 512 字节;
- 长度(14 位):这段有多少个扇区,所以一条映射最多覆盖 16383 个扇区,约 8 MB;
- 物理偏移(55 位):这段数据在本层数据区里的位置;
- 零标志(1 位):这段是不是全零。全零的段不占数据区,丢弃(discard)和写零只需要记一条映射;
- 层标签:叠层之后用来标记这条映射来自哪一层。
头尾(HeaderTrailer)以魔数 LSMT\0\1\2 开头,记录这一层的 UUID、标志位,以及索引和数据区在文件里的偏移。文件开头是头,封存后的只读层末尾还有一个结构相同的尾;打开只读层时先读尾部,就能找到索引。
读写怎么走
叠起来的一组层,底下是若干只读层,最上面是唯一的可写层(upper)。
读的时候,从上往下找:最上面一层有这段映射,就用它的数据;没有,就往下一层找;所有层都没有,就是从没写过的空洞,返回零。一次读请求可能跨好几段,每段由不同的层提供,overlaybd 把它们拼起来返回。
如果每次读都逐层查找,层数一多就会变慢。所以 overlaybd 在打开镜像时,会把所有只读层的索引合并成一张索引:对每段虚拟地址只保留最上面那一层的映射,再用层标签记住它来自哪一层。这样读的时候只需要查两张表:可写层的可变索引,以及合并好的只读索引。代码里这两者组合成 ComboIndex。
写只发生在可写层,而且只追加:新数据写到可写层数据区的末尾,再在可写层的内存索引里插入一条映射,遮住下层的旧数据。客户机发 flush 时,索引才刷到磁盘上。已经发布的只读层永远不会被改动。
快照由 create_snapshot_and_restack 完成:把当前可写层封存,变成最新的一层只读层,再在原地打开一个空的可写层。整个过程不复制数据,只是改了层的身份。下次快照记录的,就只是相对上一次又改了哪些块。
这正是分叉便宜的原因:从同一份模板或同一个快照出来的多台虚拟机,共享下面那一叠只读层文件,各自只有一个很薄的可写层。
压缩:ZFile
只读层可以压缩,格式叫 ZFile。它不是把整个文件压成一块,而是把原始数据按固定大小(默认 4 KiB)切块,每块单独用 zstd(级别 3)压缩,再附一张跳表,记录每个压缩块在文件里的位置,并为每块存 CRC32C 校验值。
读某个偏移时,先查跳表找到对应的压缩块,只读这一块、只解这一块。这样压缩和按需读取可以同时成立:远端的层文件是压缩过的,传输量小,但读一个 4 KiB 块不需要先把整层解开。
层文件从哪来:远程按需读取和本地缓存
一个 overlaybd 层的字节可能在本机,也可能在远端。overlaybd 把“怎么读一个文件”抽象成 VirtualFile 接口,后端可以替换:
- 本机文件:用 io_uring 读写,可选
O_DIRECT;
- OCI 镜像仓库:
registryfs_v2后端,用 HTTP Range 请求按需拉取层文件的某一段;
- 对象存储:快照可以放在 S3 兼容的对象存储上;
- tar:直接从 tar 包里读。
远端后端前面还有一层本地缓存,以固定大小的块为单位回填。缓存有容量上限,冷数据会被淘汰,所以集群里的镜像总量可以远大于单机磁盘。节点之间还可以开 P2P(基于 iroh),一份层文件被某台节点拉过以后,其他节点可以直接从它那里取,不必都回源。
镜像进来有两条路。如果仓库里本来就是 overlaybd 格式的镜像,aenv pull 只取清单,不下载数据,沙箱启动后由 registryfs_v2 按需拉取,后台再慢慢把完整的层下载进本地缓存。如果是普通的 OCI 镜像(tar、tar+gzip、tar+zstd),AgentENV 会按顺序把每个 tar 层解进一块 64 GiB 的虚拟盘上的 ext4,每层转换的结果存成一个 .commit 文件,按 OCI 层的摘要做内容寻址缓存。下次遇到同一个层,就不必再下载和转换。私有仓库沿用 Docker 的登录配置。
仓库里还有两种预取机制。一种是按 I/O 轨迹录制和回放:记下一次启动读了哪些块,下次启动时提前取。另一种是给内存快照用的启动包(startup pack),后面讲内存时再说。
从层文件到 /dev/vdb:ublk
到这里,overlaybd 已经能在用户态回答“虚拟盘第 N 个扇区是什么”。但 Firecracker 需要的是宿主机上的一个块设备或文件路径。把用户态程序变成块设备的桥,就是 ublk。
ublk 是什么
ublk(userspace block device)是 Linux 6.0 引入的用户态块设备框架。内核里的 ublk_drv 驱动负责对外呈现一个真正的块设备 /dev/ublkbN;所有发到这个设备上的读写,驱动不自己处理,而是转交给用户态的一个服务进程,由它决定数据从哪来、写到哪去。
在它之前,也有把块设备逻辑放到用户态的办法,比如 NBD(经过一个 socket 转发)和 TCMU(经过 SCSI 目标框架)。ublk 的不同在于它直接建立在 io_uring 上:内核和用户态之间不走 socket,而是通过 io_uring 的 URING_CMD 命令和一块共享内存交换请求,开销小得多。
一个 ublk 设备的生命周期是这样的:
- 服务进程通过 io_uring 向
/dev/ublk-control发ADD命令,描述设备的队列数、队列深度和容量。
- 内核分配设备号,创建控制设备
/dev/ublkcN和块设备/dev/ublkbN。
- 服务进程把
/dev/ublkcN上的一块 I/O 描述符数组 mmap 进来,每个队列起一个线程、一个自己的 io_uring,向内核提交一批“取请求”命令,然后等待。
- 有人读写
/dev/ublkbN时,内核把请求的操作类型、起始扇区和长度写进描述符数组,并完成对应的那条“取请求”命令,唤醒服务进程。
- 服务进程处理完,提交“完成并取下一个”命令,内核据此结束原来的块请求。
AgentENV 自己写了一个基于 tokio 的 ublk 服务端(storage/ublk/),每个设备的处理逻辑是一个 OverlaybdTarget:它把 ublk 送来的读、写、flush、discard、写零请求,分别翻译成对 overlaybd ImageFile 的读、追加写、索引落盘和零段映射。数据缓冲区可以走传统的用户态内存,也可以用 io_uring 的稀疏缓冲表做零拷贝,文档写的是内核 6.8 以上可用。
uvm-ublk-daemon:所有设备放在一个进程里
AgentENV 没有让节点进程直接管理 ublk 设备,而是起了一个长期运行的守护进程 uvm-ublk-daemon,一台机器上所有的 ublk 设备都归它管。节点通过 Unix 套接字向它发 RPC:创建根文件系统设备、创建额外磁盘、从预热池里取设备、快照时重新叠层、改变容量、删除设备。
这样拆有几个理由。一是 io_uring 和设备所有权集中在一个进程里,节点进程只需要拿着一个客户端,不用碰底层细节。二是远程 I/O 可以集中调度:从对象存储或镜像仓库读数据的 HTTP 连接,统一放在一个专用的运行时上(默认 4 个 worker),而不是绑在某个设备自己的运行时上。这是为了避开一个实际踩过的坑:opendal 和 reqwest 会把连接池里的任务钉在当初轮询它们的运行时上,设备拆除时这个运行时被销毁,别的设备还在用的连接也会跟着断掉。
三是可以做块设备预热池。ublk 设备的创建本身有开销,守护进程会预先建好一批设备;需要时通过 swap_state 把设备背后的 overlaybd 镜像原子地换掉,设备号不变,不用拆了重建。因为设备要和具体的镜像、容量对应,预热池只能在请求路径上异步补充,不能像网络槽那样提前准备一批通用的。
交给 Firecracker
块设备准备好之后,接到 Firecracker 上就很直接了:
- 节点在这个沙箱的 Firecracker 工作目录里建一个符号链接
user-rootfs,指向/dev/ublkbN。
- 通过 Firecracker 的 API 调用
PUT /drives/...,把user-rootfs注册成第二块盘:可写,direct打开(以O_DIRECT打开设备,绕过宿主机页缓存),I/O 引擎用Async(Firecracker 内部用 io_uring 发 I/O),磁盘限速在启动前就配好。工具盘则以只读、Sync引擎挂成第一块盘。
- 虚拟机启动后,客户机内核看到
/dev/vda和/dev/vdb两块 virtio-blk 盘。
宿主机的 udev 规则还会把 ublkb* 设备的 I/O 调度器设成 none:真正的调度和合并在 overlaybd 里做,内核这一层不需要再排队。
于是客户机里的一次写,要穿过这样一条链路:
这条链路看起来很长,但每一段都是异步的、基于 io_uring 的,没有额外的数据中转进程。它换来的是:Firecracker 不需要知道 overlaybd 的存在,它看到的只是一个普通的块设备;而 overlaybd 也不需要嵌进 Firecracker,可以独立升级、独立做缓存和预取。
内存也走同一条路
暂停一台虚拟机,最朴素的做法是把整份客户机内存写到磁盘上,恢复时再整份读回来。8 GB 的虚拟机暂停一次就要写 8 GB,几万台空闲环境,这个代价太大了。
AgentENV 的做法是把内存快照也做成 overlaybd 层。
暂停时,Firecracker 先做一份只含设备状态的差异快照。节点再向 Firecracker 询问哪些内存范围是脏的、哪些页实际存在,然后用 process_vm_readv 直接从 Firecracker 进程里把这些页读出来,写成一层新的 overlaybd 内存层。之前快照留下的内存层叠在下面,这一层只覆盖这次改过的范围。
恢复时,节点用这叠内存层创建一个只读的 ublk 设备,作为 File 类型的内存后端交给 Firecracker。Firecracker 以私有映射的方式把这个设备 mmap 成客户机内存:读的时候直接读设备,第一次写某一页时,内核把这一页复制成虚拟机私有的匿名页,底层设备始终不变。
关键在于共享。从同一个快照恢复的多台沙箱,共用同一个内存 ublk 设备,靠引用计数管理生命周期。设备上读过的页会留在宿主机的页缓存里,所有虚拟机都能命中;只有被写过的页,才在每台虚拟机里各占一份匿名内存。和根文件系统那块盘正好相反:用户镜像以 O_DIRECT 打开,避免客户机和宿主机各缓存一份;内存设备则恰恰要靠宿主机页缓存在虚拟机之间共享。
为了让刚恢复的虚拟机少等远端数据,仓库里还有启动包:拍完快照后,让一台一次性的虚拟机从这份快照恢复一次,记录内存设备上每一页第一次被读到的顺序,再把这些页映射到最终层文件的 256 KiB 块上,生成一份预取清单。之后真正的恢复可以按清单提前把这些块拉进缓存。
E2B 的编排器用 userfaultfd 在缺页时从内存文件里填页,AgentENV 的仓库里也有一份这样的实现(storage/uffd-core/),但不在当前构建里,文档说保留作参考。换成块设备的好处是,内存快照和磁盘快照用的是同一套层格式、同一个守护进程、同一套缓存和 P2P。
虚拟机跑久了,客户机里会堆积文件缓存。AgentENV 用 DAMON 找出冷页,再通过 Firecracker 的 balloon 设备把空闲内存还给宿主机。
快照、暂停与分叉
有了分层的磁盘和内存,几个生命周期操作就都落在了同一套机制上:
- 暂停:磁盘做一次封存叠层,内存写出一层差异,然后停掉 Firecracker 进程。
- 恢复:用这些层创建 ublk 设备,从预热池里取一个 Firecracker 进程加载快照。
- 持久快照:
aenv snapshot create把当前的内存和磁盘增量收成一份可以再启动的快照,清单记录元数据和各层;快照可以存在本机、S3 兼容的对象存储或共享文件系统上,兼容的根文件系统增量还可以推回原来的 OCI 仓库。
- 分叉:源沙箱短暂暂停,抓一份克隆后继续运行;子沙箱在同一节点上继承文件系统、内存和资源配置,一次最多 16 个。谁先改,谁才得到自己的那一页或那一块。
分叉不能跨机器,因为它共享的正是这台机器上的层文件和页缓存。官方博客把分叉对应到多轨迹采样和树搜索:从同一个中间状态出发,同时试几种做法。
模板构建也复用这套机制。aenv pull 把一个 OCI 镜像变成模板;更复杂的环境可以写类似 Dockerfile 的 RUN、ENV、WORKDIR 步骤,构建器从基础环境拉起一台临时沙箱,逐条执行,就绪检查通过后暂停,把这一刻的状态提交成新模板。
网络:每个沙箱一个槽
网络不是这篇文章的重点,简单说一下。每个运行中的沙箱有自己的网络命名空间,Firecracker 的 TAP 设备放在里面,命名空间再通过一对 veth 接回宿主机。客户机地址固定是 169.254.0.21,宿主机侧从 10.12.0.0/16 里为每个槽切一个 /31。内部网段在任何用户策略之前就被拒绝,所以一台沙箱碰不到另一台。网络槽也能放回预热池,下一个沙箱只需要换掉用户规则。
出站策略写在命名空间里的 iptables 链上。需要按域名控制时,命名空间把 TCP 80 和 443 重定向到本命名空间内的一个透明代理,代理从 HTTP Host 或 TLS SNI 里取出域名,按策略决定放行还是拒绝。它不解密 TLS,也不改请求头。
数字
官方博客统计了高负载期间 70 台节点、大约 22.5 万个环境:申请量和宿主机实际用量之比,CPU 平均 27.9 倍(最低 14.5 倍),内存平均 9.6 倍(最低 5.7 倍)。一台 128 核、512 GB 的机器,在每个环境 2 vCPU / 4 GB 或 4 vCPU / 8 GB 的配置下,可以稳定同时运行 400 个;生产集群最多到过大约 3 万个。
延迟方面,博客写的是:基于模板的启动或恢复最快 49 毫秒,创建快照最快 133 毫秒,并发分叉摊到每个子环境最快 122 毫秒。README 给的是另一组口径:快照启动或恢复在 50 毫秒内,暂停和增量快照都在 100 毫秒内。两组数字都是项目自己测的,测量条件不同,不宜混在一起比较。
这些数字背后,就是前面讲的那几件事:空闲虚拟机的内存还给宿主机,同一份快照的干净页在页缓存里共享,磁盘只存改过的块。
和其他方案的对比
| AgentENV | 其他方案 | |
|---|---|---|
| 隔离 | 只有 Firecracker 微虚拟机(x86_64 上可选 PVM) | DSec 按任务在函数调用、容器、Firecracker、QEMU 之间选;OpenSandbox 可选 runc、gVisor、Kata、Firecracker |
| 磁盘共享 | overlaybd 分层块设备,经 ublk 呈现成一块盘 | CubeSandbox 用 XFS reflink;DSec 的容器用 EROFS + overlayfs,数据在 3FS |
| 内存共享 | 同一快照共用一个只读内存 ublk 设备,写时复制成匿名页 | E2B 用 userfaultfd;DSec 用 virtio-pmem 共享只读镜像的页缓存 |
| 分叉 | 同节点,一次最多 16 个 | 多数方案没有这个原语 |
| 调度 | 轮询或随机 | DSec 抽样后选最空的节点;OpenSandbox 的 FastSandbox 优先选已缓存镜像的节点 |
值得一提的是,DeepSeek 的 DSec 论文写明,它的 microVM 可写盘用的也是 overlaybd 加 ublk,Rust 实现就在 AgentENV 仓库的 storage/overlaybd 里。两边共享的是这套块存储代码,控制面和调度各不相同。
参考
- KVCache.AI,AgentENV: When LLMs Learn to Get the Job Done,2026-07-27。规模、延迟和成本估算。
- 月之暗面,Kimi K3 开放日,2026-07-27。
- kvcache-ai/AgentENV。
storage/overlaybd、storage/ublk、storage/ublk-daemon、tools-image/init。
- AgentENV 架构文档。节点子系统、LSMT 格式、ublk 守护进程、内存快照、Gateway 和 Scheduler。
- 沙箱概念。状态机与分叉上限。
- Li 等,DADI: Block-Level Image Service for Agile and Elastic Application Deployment,USENIX ATC 2020。
- Linux 内核文档,Userspace block device driver (ublk driver)。
- Agache 等,Firecracker: Lightweight Virtualization for Serverless Applications,NSDI 2020。
- E2B。API 形状和 envd 的来源。
- DeepSeek-AI 等,DSec,arXiv:2609.22978。