<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="assets/css/xml/style.xslt"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>高策</title>
    <description>江湖小虾米</description>    
    <link>https://gaocegege.com/Blog/</link>
    <atom:link href="https://gaocegege.com/Blog/rss.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Agent sandbox 可能的选型以及 unikernel 的机会</title>
        <description>&lt;p&gt;最近放假，研究 agent 上瘾。&lt;a href=&quot;https://gaocegege.com/Blog/jiucai-rl&quot;&gt;刚刚体验了 agent rl&lt;/a&gt;，就又在看 agent sandbox 方案时，一个问题反复浮现：既然 agent 要的是轻量、安全、快速启动，为什么没人用 unikernel？在 2017 年的时候我写过一篇文章介绍过 unikernel：&lt;a href=&quot;https://gaocegege.com/Blog/%E5%AE%89%E5%88%A9/unikernel-book&quot;&gt;Unikernel：从不入门到入门&lt;/a&gt;。还在读书时候我就一直觉得它是非常有趣的技术，可惜一直没有真正被业界广泛采用过。&lt;/p&gt;

&lt;p&gt;从理论上看，这简直是天作之合。unikernel 将应用和内核编译成一个单一的、运行在硬件虚拟化之上的镜像——没有多余的 shell，没有无用的系统调用，攻击面小到极致，启动快到毫秒级。对于运行不可信代码的 Agent 沙箱来说，还有比这更理想的基础吗？&lt;/p&gt;

&lt;p&gt;然而，现实是不仅没人用，甚至很少有人讨论它。当行业在为 agent 寻找安全底座时，我们看到的是 firecracker、gvisor、kata containers，可能再带上 wasm。&lt;/p&gt;

&lt;p&gt;在讲这个问题之前，先让我们简单统一一下 agent sandbox 的需求。agent sandbox 需要满足以下几个核心需求。首先是冷启动时间要快，理想情况下在 100ms 以内。其次是安全性要高，能够有效隔离不可信代码，防止越权访问和攻击。第三是要能支持 python 这种主流的 agent 开发语言，能够兼容常见的 python 库和工具。第四是要有一个方便的镜像构建流程，能够让用户快速构建和部署自己的 agent 镜像。&lt;/p&gt;

&lt;h2 id=&quot;e2b-firecracker&quot;&gt;e2b (firecracker)&lt;/h2&gt;

&lt;p&gt;先来看看用的最多的两个思路，容器和 firecracker。我觉得很多人会误会容器的启动时间不如 firecracker 之类的轻量级虚拟机快，比如我在研究 sandbox 时候看到的&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1999938129465979624&quot;&gt;知乎上的一篇文章&lt;/a&gt;。可能是因为用容器的时候我们跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt;，体感启动时间很漫长。但排除掉镜像拉取需要的诸多操作，容器启动做的事情非常简单，基本就是&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;镜像解压与层合并：overlay2 存储驱动将只读层与可写层合并&lt;/li&gt;
  &lt;li&gt;容器创建：runc 创建命名空间、cgroups、网络&lt;/li&gt;
  &lt;li&gt;进程启动：执行 ENTRYPOINT/CMD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个启动的时间在 50ms 以下，甚至可以做到 10ms 级别。相比之下，firecracker 之类的轻量级虚拟机虽然也很快，但通常在 125ms 以上。还有很多比较是拿容器里的进程就绪的时间和一个空的 micro vm 的启动时间来比较的，这样的比较也是不公平的。容器的启动时间完全可以满足 agent sandbox 的需求了。它问题更多来自于安全性。虽然容器通过命名空间和 cgroups 提供了一定程度的隔离，但它们仍然共享宿主机的内核。&lt;/p&gt;

&lt;p&gt;firecracker 之类的轻量级虚拟机则提供了更强的隔离，因为它们运行在完全独立的内核上，攻击面更小，更适合运行不可信代码的场景。我们可以拿 e2b 作为一个例子来看看它是如何利用 firecracker 来构建 agent sandbox 的。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/e2b-dev/e2b&quot;&gt;e2b&lt;/a&gt; 是最近比较火的一个 agent sandbox 方案，它的设计目标是为 agent 提供一个轻量级、安全的运行环境，支持快速启动和高密度部署。我们来看看它是怎么构建 sandbox 的。&lt;/p&gt;

&lt;p&gt;在 e2b 里，sandbox 的镜像被称作 template：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Snapshots are saved running sandboxes. We serialize and save the whole sandbox’s filesystem together with all the running processes in a way that can be loaded later.
This allows us to load the sandbox in a few hundred milliseconds any time later with all the processes already running and the filesystem exactly as it was.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;因为 e2b 是用 firecracker 来实现 sandbox 的，所以它的 template 就是一个完整的 Linux 镜像。而 e2b 给到用户的构建语言又是 Dockerfile 的一个子集，它是把一个 docker 的文件系统 extract 成了 ext4 rootfs，实现藏的比较深，在 &lt;a href=&quot;https://github.com/e2b-dev/infra/blob/main/packages/orchestrator/internal/template/build/builder.go#L101&quot;&gt;e2b-dev/infra&lt;/a&gt; 下，大概流程是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;拉取基础镜像：从远程仓库获取指定的 Docker 镜像&lt;/li&gt;
  &lt;li&gt;注入配置层：添加新的文件层，包含主机名、DNS、Envd 服务配置以及基础预置脚本（该脚本会在大部分 VM 服务启动前运行）&lt;/li&gt;
  &lt;li&gt;提取文件系统：从镜像中提取 ext4 格式的根文件系统，这个过程类似 &lt;a href=&quot;https://github.com/rust-firecracker/buildfs/&quot;&gt;buildfs&lt;/a&gt; 的实现&lt;/li&gt;
  &lt;li&gt;初次启动（预置阶段）：启动 Firecracker 微虚拟机，运行仅包含预置脚本的 BusyBox init 进程。此步骤会安装 systemd（用于后续正式的 VM 启动），等待脚本执行完成后退出&lt;/li&gt;
  &lt;li&gt;二次启动（服务初始化）：再次启动 Firecracker 虚拟机（这次使用 systemd），等待 Envd 服务就绪&lt;/li&gt;
  &lt;li&gt;构建模板层：生成模板所需的各个层/步骤&lt;/li&gt;
  &lt;li&gt;沙箱内收尾配置：重启沙箱环境，执行两个额外的命令：
    &lt;ul&gt;
      &lt;li&gt;配置脚本：启用 swap、创建用户、修改文件夹权限等&lt;/li&gt;
      &lt;li&gt;启动命令（如果定义了）以及就绪命令（未定义时使用默认值）&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;创建快照：对配置完成的系统进行快照&lt;/li&gt;
  &lt;li&gt;上传模板：将模板及所有尚未上传的层上传到存储系统&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过这样的方式，e2b 实现了一个基于 firecracker 的 agent sandbox。它是如何在一个集群上调度这些 sandbox 的呢？它的调度系统叫做 orchestrator，负责管理 sandbox 的生命周期，包括创建、销毁、监控等。&lt;/p&gt;

&lt;p&gt;很多人可能看到 e2b 代码里的 nomad 就会觉得它是基于 nomad 来做调度的，但实际上它并没有直接使用 nomad 的调度功能，而是自己实现了一个非常简单的调度器，来管理 sandbox 的生命周期。nomad 只用来管理 e2b 控制平面的部署和扩展。&lt;/p&gt;

&lt;p&gt;e2b 调度 sandbox 的过程非常简单，是基于 &lt;a href=&quot;https://github.com/e2b-dev/infra/blob/d79e4ca97205d304d8116a78d8f4d485bc644cfe/packages/api/internal/orchestrator/placement/placement_best_of_K.go#L106&quot;&gt;best of k&lt;/a&gt; 的算法。当需要创建一个 sandbox 的时候，orchestrator 会从集群里随机选取 k 个节点，然后在这 k 个节点上基于预设 heuristic（如已用 CPU、内存、在起沙箱数等）计算一个 score，越低越好（负载最小化），最后选取 score 最低的那个节点来创建 sandbox。这个算法虽然适合沙箱高并发生命周期短的 agent 场景，调度效率也很高，但是应该还有很大优化的空间。&lt;/p&gt;

&lt;p&gt;这里引申出来一个我的观点，我一直觉得 Kubernetes 并不适合 agent sandbox 的调度。Kubernetes 适合长期运行的服务，或者说生命周期比较长的工作负载。agent sandbox 的生命周期非常短。而且 Kubernetes 太重了，agent 很多情况下需要在本地运行，或者说在一个非常小的集群里运行，又或者需要在本地和 cloud 之间无缝迁移，互相配合。e2b 这种自己实现一个轻量级的调度器，来管理 sandbox 的生命周期，也是一种不错的选择。&lt;/p&gt;

&lt;p&gt;当然作为 infra 哥，时时刻刻想着能不能把其中的共性抽象成一个通用的调度系统，来支持不同的 sandbox 方案。可能最下面的 data plane 的预设是 containerd-based 的 sandbox runtime，比如 containerd-firecracker。调度上可以做的 tradeoff 很多。这个跟这篇文章的主题关系不大，就不展开了。&lt;/p&gt;

&lt;p&gt;除了启动快之外，firecracker 还带来了 snapshot 的能力，可以 &amp;lt;1s（之前文档里的数据是 intel &amp;lt;8ms, amd &amp;lt;3ms）的时间来 resume 一个已经运行好的 sandbox，这对于 agent 来说是非常有吸引力的。因为 agent 的生命周期短，频繁的创建和销毁会带来不小的性能开销，而 snapshot 能够大大减少这个开销。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;e2b_code_interpreter&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Sandbox&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;sbx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Sandbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Sandbox created&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sbx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sandbox_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Pause the sandbox
# You can save the sandbox ID in your database to resume the sandbox later
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sbx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;beta_pause&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Sandbox paused&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sbx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sandbox_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Connect to the sandbox (it will automatically resume the sandbox, if paused)
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;same_sbx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sbx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Connected to the sandbox&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;same_sbx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sandbox_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;k7kata&quot;&gt;k7（kata）&lt;/h2&gt;

&lt;p&gt;看完 e2b 基于自己维护的 docker image 转 kernel image 用 firecracker 和自己的 orchestrator 来管理 sandbox 的方案，再来看一个基于 kata 的实现 &lt;a href=&quot;https://github.com/Katakate/k7&quot;&gt;k7&lt;/a&gt;。国内也有很多大厂的 agent sandbox 方案也是基于 kata 来实现的，我们可以把 k7 作为一个代表来分享一下观点。&lt;/p&gt;

&lt;p&gt;k7 的 vmm 也是 firecracker，没有用 cloud hypervisor。由于 kata 是支持兼容 OCI 标准的容器镜像的，所以 k7 的 template 就是一个普通的 docker 镜像了。kata 跟 Kubernetes 的相性非常好，k7 直接使用了 Kubernetes(or k3s) 来做调度。&lt;/p&gt;

&lt;p&gt;整体因为采用了成熟的组件，k7 的实现相对简单了很多。它的 sandbox 镜像构建流程也非常简单，基本上就是基于 dockerfile 来构建一个 docker 镜像，然后直接用这个镜像来启动 sandbox 就好了。相比 e2b 那种需要把 docker 镜像 extract 成 ext4 rootfs 的方式，k7 的方式要简单很多了。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;katakate&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Client&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;k7&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;https://&amp;lt;your-endpoint&amp;gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
  &lt;span class=&quot;n&quot;&gt;api_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;your-key&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create sandbox
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;my-sandbox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;alpine:latest&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Execute code
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;echo &quot;Hello World&quot;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;stdout&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# List all sandboxes
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sandboxes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Delete sandbox
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是它应该无法像 e2b 那样利用 firecracker 的 snapshot 来实现快速的 resume 了。kata 的设计是为了兼容容器，它应该是屏蔽了 firecracker 的 resume 功能，而且 kata 的实现是 VM 里跑了一个 container，我也不确定直接用 firecracker snapshot 是不是能取得预期的效果和延迟。有 kata 的朋友可以分享一下。&lt;/p&gt;

&lt;p&gt;在我自己看来，resume 的能力是不是 agent sandbox 需要的核心能力，可能还要打个问号。对于一些简单的 agent 场景，可能并不需要 snapshot 的能力。而 kata 带来的完全兼容 OCI 镜像的能力，可能对于很多用户来说更有吸引力了。毕竟构建一个 docker 镜像要比构建一个 firecracker 的 template 要简单很多了，速度也快得多。e2b 的实现是用构建时间来换 micro vm 的启动时间和 snapshot resume 的便捷。&lt;/p&gt;

&lt;h2 id=&quot;montywasm&quot;&gt;monty（wasm）&lt;/h2&gt;

&lt;p&gt;看完 vm 的方案，再来看看 wasm 有没有什么好设计。wasm 能够比容器冷启动还要快，前面提到容器的启动时间在 50ms，中间要配置 cgroups，命名空间等等，而 wasm 的启动时间在 10ms 级别，它直接在运行时内实例化，没有内核加载、设备初始化等开销。&lt;/p&gt;

&lt;p&gt;但是 wasm 支持 python 是非常麻烦的，首先 wasm 的假设是单一线性的内存空间，而 python 的内存管理依赖 gc，还挺复杂（我不懂）。其次跟 unikernel 比较像的一点是 wasm 通过 WASI 访问系统，WASI 是一个精简、可移植的接口层，功能远少于 Linux 原生接口。python 高度依赖各种系统调用和库函数，WASI 的限制会导致很多 python 库无法正常工作。&lt;/p&gt;

&lt;p&gt;Pyodide 是一个将 CPython 解释器和科学计算生态的一些项目编译到 WebAssembly 的项目，但它的冷启动特别慢，因为需要编译整个 CPython 解释器和相关库到 wasm，这个过程非常耗时。目前优化的比较好的思路是&lt;a href=&quot;https://developers.cloudflare.com/workers/languages/python/how-python-workers-work/&quot;&gt; Cloudflare 的思路&lt;/a&gt;，它通过 snapshot 把运行时的耗时挪到了部署时。&lt;/p&gt;

&lt;p&gt;wasmer 也做过一些优化，也是通过类似于快照和缓存的思路。但是这些在我看来都是治标不治本的方案。wasm 的设计初衷并不是为了支持像 python 这样复杂的动态语言的，所以它在设计上就有一些限制。&lt;/p&gt;

&lt;p&gt;比较值得一提的是 &lt;a href=&quot;https://github.com/pydantic/monty&quot;&gt;monty&lt;/a&gt;。它是一个支持 python 部分子集的解释器。跟一些常见的技术对比如图所示。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/unikernel-agent/table.png&quot; alt=&quot;Monty 对比&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Monty 对比&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;0.06ms 的启动时间远远快于容器和 firecracker 之类的方案了。其中 starlark 也是一个 python 的方言，是 bazel 用来作为构建语言提出的一个子集语言。在我们曾经的项目 &lt;a href=&quot;https://github.com/tensorchord/envd&quot;&gt;envd&lt;/a&gt; 里也有用过 starlark 作为构建语言。这个支持的语法非常有限，虽然冷启动快但是没有参考性。&lt;/p&gt;

&lt;p&gt;其中提到的 wasmer 虽然快，但是本身我还是觉得不值得考虑，原因前面有提到，再加上本身项目不怎么维护了。其中的 sandboxing service 举例提到了 e2b，daytona 和 modal。这三个其实技术完全不一样，这部分测试也比较随意，不具备参考性。YOLO python 就是直接本地跑 python 解释器了，虽然启动时间快，但是安全性完全没有保障。&lt;/p&gt;

&lt;p&gt;看上去 monty 是一个不错的方案，但是它支持的语法子集太小，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class&lt;/code&gt; 不支持，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sys&lt;/code&gt; 等模块也不支持，这样的阉割下，unikernel 反而感觉更有优势。&lt;/p&gt;

&lt;h2 id=&quot;unikernel&quot;&gt;unikernel&lt;/h2&gt;

&lt;p&gt;看完这些项目的设计，我们再回到最开始的问题，为什么没人用 unikernel 来做 agent sandbox 呢？一个挺重要的原因我认为是之前的 unikernel 是不支持多进程的。传统意义上的 unikernel 没有内核和用户态的概念，应用与内核编译为一体，它也是单地址空间，没有多进程的概念。如果没有多进程的概念，python 这种语言就没法很好的支持，因为它重度依赖多进程模型。&lt;/p&gt;

&lt;p&gt;unikraft 在去年五月&lt;a href=&quot;https://unikraft.org/blog/2025-05-15-multiprocess?trk=public_post_comment-text&quot;&gt; 0.19 版本支持了多进程&lt;/a&gt;，这对支持 python 来说是一个非常重要的特性，但它实现的方式是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfork&lt;/code&gt; 后子进程与父进程共享地址空间，如果子进程不立刻 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execve&lt;/code&gt; 的话，父子进程访问内存就会互相干扰。虽然 unikraft 的实现是为了兼容一些需要多进程的应用，但它并不是真正意义上的多进程。&lt;/p&gt;

&lt;p&gt;还有的探索是 &lt;a href=&quot;https://ar5iv.labs.arxiv.org/html/2206.00789&quot;&gt;Unikernel Linux (UKL)&lt;/a&gt;。UKL 采取了一个非常不同的思路，它不是从零开始设计 unikernel，而是通过配置选项将 unikernel 优化技术集成到 Linux 中。在 UKL 中，单个应用可以直接链接到 Linux 内核，在 supervisor 权限下运行，无需修改应用源代码，只需重新编译并与修改过的 Linux 内核和 glibc 联接。&lt;/p&gt;

&lt;p&gt;UKL 的基础模型保留了 Linux 的大部分设计，包括应用和内核的分离地址空间、不同的执行模式、以及多进程支持的能力。这样做的好处是未修改的应用可以开箱即用地运行。但是 UKL 失去了 unikernel 的一些核心优势，比如极小的攻击面和极快的启动时间。&lt;/p&gt;

&lt;p&gt;但另外一个角度，我们不能只看到启动时间，事实上构建和 image 分发的效率也是非常重要的。docker 之所以能够成为主流的虚拟化技术，镜像分发的方便和高效是一个非常重要的因素。unikernel 能够做到非常小的镜像大小，是非常有竞争力的。如果能够在多进程与小镜像之间做一个好的取舍，我还是认为 unikernel 是非常有潜力的。&lt;/p&gt;

&lt;h2 id=&quot;modal&quot;&gt;modal&lt;/h2&gt;

&lt;p&gt;讲到这里，我们再讲一个在镜像上做了非常扎实工作的产品 modal。他们之前在 Youtube 上有一个专门讲加速的视频 &lt;a href=&quot;https://www.youtube.com/watch?v=SlkEW4C2kd4&quot;&gt;Fast, lazy container loading in modal.com by Jonathon Belotti&lt;/a&gt;。实际上镜像带来的启动时间开销是远大于启动本身的，这才是延迟最大的单一来源。&lt;/p&gt;

&lt;p&gt;传统的 docker 拉取镜像，是串行完成下载 N 个 gzip 压缩层（速度约 2 GiB/s）、单线程解压（80 MiB/s）、解包到文件系统这一整套流程。对于一个 8GB 的镜像，这个过程可能需要一分钟。整个容器在数据完全就绪前，无法启动。&lt;/p&gt;

&lt;p&gt;modal 采取了 lazy loading 的方式，换句话说就是按需加载。当你运行一个 pytorch 进程的时候，你可能不会访问容器文件系统里的所有文件。这个思路跟 &lt;a href=&quot;https://github.com/dragonflydb/dragonfly&quot;&gt;dragonfly&lt;/a&gt; / estargz 之类的分层镜像格式是类似的，但是 modal 的实现不太一样，是通过 fuse 来实现的，我猜测应该是用 fuse 拦截文件系统的读取，在创建容器文件系统的时候，根据镜像的元数据，先生成一个占位的文件系统树，然后在被访问的时候按照内存，本地 SSD，同可用区缓存服务器，区域 CDN，对象存储的优先级顺序，去请求数据。&lt;/p&gt;

&lt;p&gt;视频里还提到很多 fuse 的性能调优，感兴趣可以自己去看看。modal 的这个设计确实非常巧妙，能够大大减少镜像拉取的时间，提升容器的启动速度。&lt;/p&gt;

&lt;h2 id=&quot;结&quot;&gt;结&lt;/h2&gt;

&lt;p&gt;agent sandbox 的设计有很多不同的方案，每种方案都有自己的优缺点。firecracker 提供了强隔离和 snapshot 能力，但构建镜像比较麻烦；kata 提供了兼容 OCI 镜像的能力，但可能无法利用 firecracker 的 snapshot；wasm 启动快但支持 python 很麻烦；unikernel 理论上非常适合 agent sandbox，但之前不支持多进程是一个很大的限制。未来随着 unikernel 技术的发展，可能会有更多的机会来支持 agent sandbox 的需求。&lt;/p&gt;

&lt;p&gt;但是不得不说，镜像构建和分发的效率也很重要。当然这个对于 modal 会运行 LLM 推理服务的产品来说更加重要，因为 LLM 的镜像和模型都很大。而 agent 可能没有那么夸张。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 17 Feb 2026 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/unikernel-agent</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/unikernel-agent</guid>
      </item>
    
      <item>
        <title>从 vibe coding agent 到后训练，从零开始的实验科学</title>
        <description>&lt;p&gt;最近假期有时间，跟几个做 Agent 的朋友聊天，他们都提到了一个共同的问题：负责决策规划的主 agent 需要维护一个状态机来追踪环境状态和行为。这个状态机通常是文本格式，记录各种状态、事件和转移规则，主 agent 根据 subagent 的输出来更新它并做决策。&lt;/p&gt;

&lt;p&gt;现实中经常出现两个问题。首先，主 agent 一旦发现 subagent 的输出不符合预期，就容易自己出手去执行，结果导致人格漂移，自己代入到 subagent 的角色里去了，忘了自己应该做什么——这可能是指令依从的问题。其次，主 agent 经常会丢失之前的状态记忆，导致状态机的状态不准确——这可能是上下文窗口不足的问题。&lt;/p&gt;

&lt;p&gt;于是我自然而然想到，能不能通过后训练，让模型学会一个状态机的形式化描述。主 agent 其实是整个架构的瓶颈——所有决策都要它来做，所有的信息流都是同步的，subagent 的输出全部要喂给它，它的输出又要喂给其他 subagent，根本没法 streaming。这样一来效率就很低。&lt;/p&gt;

&lt;p&gt;要是能训练一个小模型，比如 3B、7B，专门去学怎么描述和更新状态机，那主 agent 就能专注于决策规划，不用老是想着接管 subagent 的工作。而且作为系统的性能瓶颈，主 agent 本身用小模型推理也会快很多。&lt;/p&gt;

&lt;p&gt;决定场景后，我开始研究是 SFT 还是 RLHF。因为之前没有过经验，我发现自己很难判断哪个更适合这个任务。而且这个场景还涉及到要预先定义一个状态机的形式化描述，这个描述本身就很复杂，可能需要一些专业知识来设计。问题对于第一次训练的我来说有点过于复杂。于是我决定先换个问题，先从一个更简单的任务开始，来体验一下后训练的过程。&lt;/p&gt;

&lt;h2 id=&quot;vibe-coding-agent&quot;&gt;Vibe coding agent&lt;/h2&gt;

&lt;p&gt;于是我随便选了一个 idea，花了一个小时的时间，vibe coding 了一个辅助 A 股投资的 agent。这个 agent 的功能很简单，就是根据用户输入的股票代码，返回该股票的基本信息、历史价格，以及一些技术指标。具体的 tool 就像下面这样：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# 工具使用指南

## 可用工具概览

### 🔍 股票搜索工具

**search_stock_by_name(stock_name)** - A股名称搜索
- 用途：根据A股股票名称搜索股票代码
- 何时使用：用户只提供A股公司名称，不知道代码时
- 示例：`search_stock_by_name(&quot;茅台&quot;)` → 找到 600519

### 📋 基础信息工具

**get_stock_info(stock_code)** - A股基本信息
- 用途：查询A股股票的公司信息、上市信息、主营业务等
- 何时使用：用户首次询问某只A股股票时，或需要了解公司背景时
- 示例：`get_stock_info(&quot;600519&quot;)` → 获取贵州茅台的基本信息

### 📊 实时行情工具

**get_realtime_quote(stock_code)** - A股实时行情
- 用途：获取A股股票当前的实时价格、涨跌幅、成交量等
- 何时使用：用户问A股&quot;现在多少钱&quot;、&quot;今天涨了吗&quot;等
- 示例：`get_realtime_quote(&quot;600519&quot;)` → 获取茅台当前价格（人民币）
- 返回：当前价、涨跌额、涨跌幅、今开、最高、最低、昨收、成交量、成交额、换手率、市盈率、市净率

...

### 📈 历史数据工具

**get_kline_data(stock_code, k_type=&quot;日线&quot;, start_date=&quot;&quot;)** - A股K线
- 用途：获取A股历史K线数据，用于分析趋势
- 何时使用：需要查看A股历史走势、画K线图时
- 参数：
  - k_type: &quot;日线&quot;、&quot;周线&quot;、&quot;月线&quot;
  - start_date: &quot;2024-01-01&quot; 格式
- 示例：`get_kline_data(&quot;600519&quot;, &quot;日线&quot;, &quot;2024-01-01&quot;)`

...

### 📐 技术指标工具

**calculate_ma(stock_code, periods=&quot;5,10,20,60&quot;, start_date=&quot;&quot;)**
- 用途：计算移动平均线（MA5, MA10, MA20, MA60）
- 何时使用：技术分析，查看均线支撑/压力位
- 示例：`calculate_ma(&quot;600519&quot;, &quot;5,10,20,60&quot;)`

**calculate_macd(stock_code, short_period=12, long_period=26, signal_period=9)**
- 用途：计算MACD指标，判断趋势强度和买卖点
- 何时使用：需要确认趋势转折点时
- 示例：`calculate_macd(&quot;600519&quot;)`

**calculate_bollinger_bands(stock_code, period=20, num_std=2, start_date=&quot;&quot;)**
- 用途：计算布林带指标，判断股票的波动性和支撑/压力位
- 何时使用：判断价格可能的涨跌幅，找支撑压力位
- 示例：`calculate_bollinger_bands(&quot;600519&quot;, 20, 2)`

**analyze_volume_trend(stock_code, period=20, start_date=&quot;&quot;)**
- 用途：分析股票的成交量趋势，判断价量关系和市场参与度
- 何时使用：确认趋势真实性，判断主力资金进出
- 示例：`analyze_volume_trend(&quot;600519&quot;, 20)`

...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/rl/screenshot.jpg&quot; alt=&quot;茅台&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;茅台的分析&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Vibe coding 这个 agent 的过程也非常有趣，因为涉及到多轮的交互和工具调用，虽然只是个单 agent 的实现，但是调试起来也特别麻烦。模型中间哪一轮出了问题，都会导致最后的输出不正确。&lt;/p&gt;

&lt;p&gt;这个也是为什么 multi-turn RL 难训练的原因之一，最后的奖励 credit 到底是哪一轮对话贡献最大，或者说是哪一轮工具调用贡献最大，都是很难判断的。跟望哥交流他提到微软有个工作是每个 step 都能够有一个稠密的 reward 来指导训练，而不像现在这种 sparse reward 的情况，只有最后的结果是对的或者错的，导致训练非常困难，我挺上去感觉确实是很好的思路。&lt;/p&gt;

&lt;p&gt;回到这个 agent 的实现，为了解决调试难的问题，我开始找有没有用来做调试的工具，结果发现了岛娘在月之暗面开源的 &lt;a href=&quot;https://github.com/MoonshotAI/moonpalace/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MoonPalace&lt;/code&gt;&lt;/a&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;- 简单易用，启动后将 base_url 替换为 http://localhost:9988 即可开始调试；
- 捕获完整请求，包括网络错误时的“事故现场”；
- 通过 request_id、chatcmpl_id 快速检索、查看请求信息；
- 一键导出 BadCase 结构化上报数据，帮助 Kimi 完善模型能力；
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个工具非常适合调试 agent 的问题，因为它能够捕获完整的请求信息，包括模型的输入输出、工具调用的参数和结果，以及网络错误等情况。通过 request_id 和 chatcmpl_id，我可以快速定位到哪一轮对话出了问题，查看具体的请求信息，分析模型的行为。但是比较可惜，岛娘说没有动力支持更多模型。虽然它应该是兼容 OpenAI API 的，但我也没继续尝试了。&lt;/p&gt;

&lt;p&gt;我觉得目前还非常缺乏一个帮助开发者 troubleshoot agent 的工具，尤其是对于 multi-turn 的 agent 来说，调试起来非常麻烦。我们需要一个类似于 GDB 的 debugger，能够让我们在每一轮对话中查看模型的输入输出、工具调用的参数和结果，以及记忆状态等信息，同时能够决定 hang 在哪一轮对话，修改模型的输入输出，来帮助我们更好地理解模型的行为和调试问题。&lt;/p&gt;

&lt;p&gt;期间我又去试了试 litellm，但它更像是一个网关，不是为了调试 agent 设计的，登录它的 dashboard 还需要一个 postgres 数据库，感觉有点重了。&lt;/p&gt;

&lt;p&gt;于是我就又为了调试这个 agent，vibe coding 了一个简单的 debugger。这个 debugger 只花了半小时左右的时间，它能够把每次的请求信息存到 sqlite 数据库里，OpenAI 的 chat completion API 的请求和响应都被记录下来，可以根据时间查询。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/rl/debugger.png&quot; alt=&quot;debugger&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Debugger&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Vibe coding 有一种魔力，让你快速地把一个想法实现出来，虽然可能不够完美，但能够满足 90% 的需求。会让人沉浸于不停构建新东西的快感中，完全不想停下来去做其他的事情。而且遇到了问题，就又想去 vibe coding 一个工具来解决问题，这样就形成了循环。&lt;/p&gt;

&lt;h2 id=&quot;sft&quot;&gt;SFT&lt;/h2&gt;

&lt;p&gt;这个 agent 最开始是直接 ReAct 来实现的，用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-chat&lt;/code&gt; 工作的非常好，完成率能够在 80% 以上。用户输入一个股票代码，agent 就会调用工具来获取信息，然后返回给用户。换了 7B 小模型之后，完成率掉到了 60% 左右。尤其是在需要区分港股和 A 股的场景下，模型经常会搞混，导致调用了错误的工具。&lt;/p&gt;

&lt;p&gt;后训练的机会就来了。我想先 SFT 试试看。作为 Infra 哥，我第一件事就是先去看了 verl 和 slime 这两个后训练的框架。望哥在 Seed 有参与过 verl 的工作，我在搞 agent 的时候也一直在请教他。slime 本身是我们在腾讯交流蛮多的前同事 Zilin Zhu 参与设计和开发的，还挺有缘份。&lt;/p&gt;

&lt;p&gt;至今还没研究很明白这两个框架的区别和适用场景，但我看到 verl 做 SFT 不需要依赖 Ray，直接 torchrun 就够了，而&lt;a href=&quot;https://github.com/THUDM/slime/blob/main/examples/geo3k_vlm/run_geo3k_vlm_sft.sh&quot;&gt; slime 做 sft 需要 ray&lt;/a&gt;，我就想先试试 verl。虽然我觉得 Ray 挺有趣的，&lt;a href=&quot;https://gaocegege.com/Blog/why-do-i-like-ray&quot;&gt;20 年就写文章讲过它&lt;/a&gt;，但它本身也是个挺复杂的系统，万一遇到了什么问题，会更加麻烦。&lt;/p&gt;

&lt;p&gt;接下来就是计算一下需要什么样的显卡来训练。7B 的模型，本身 16 位参数占 14GB，梯度 14GB，优化器存动量、方差、参数 32 位，7B * 4 * 3 = 84GB，一共 112GB 左右。FSDP 可以通过分片的方式降低显存需求，比如说分成两片，也就是每张卡 56GB。同时 FSDP 还可以 &lt;a href=&quot;https://huggingface.co/docs/transformers/fsdp#cpu-offload&quot;&gt;CPU offload&lt;/a&gt;，参数和梯度不用的时候 offload 到 CPU。并且还有 gradient checkpointing，反向传播时不保存中间激活值，需要重新计算前向传播来节省显存。所以 GPT 告诉我两张 24GB 的卡就够了。&lt;/p&gt;

&lt;p&gt;我最开始找了一个双 L4 的机器，结果跑不起来。我因为一直就怀疑显存太少，所以换了一个四卡 L4 的机器，可以运行。但是我后来才发现最开始我 gradient checkpointing 没开，所以双卡能不能跑起来我也不确定了。&lt;/p&gt;

&lt;p&gt;对于训练的数据库，我是用 deepseek 生成了 1000 条左右。每条数据包含用户输入的股票代码、模型调用工具的日志（包括调用了哪个工具，传了什么参数，得到了什么结果），以及模型最终返回给用户的输出。训练完之后惊喜地发现，tool use 的水平提高了，但是模型中间的推理变弱了。还是回到区分 A 股和港股这个问题上，模型虽然学会了调用正确的工具，但在推理过程中经常卡在某个环节要确认好多次。比如要来回确认是不是港股，确认后拿到工具返回，就又当成 A 股，还要下次再分析推理确认一次。&lt;/p&gt;

&lt;p&gt;我以为是我训练数据构造的有问题，我又 SMOTE 过采样了一下，生成了相对强调推理的 500 条训练数据，从头开始训练。结果推理能力好像强一点了，但是使用工具的能力又不行了。有一篇&lt;a href=&quot;https://arxiv.org/pdf/2602.00994&quot;&gt;最近的文章&lt;/a&gt; 解释说推理和工具使用的梯度方向不一致，联合训练会互相干扰。&lt;/p&gt;

&lt;p&gt;我不确定是不是这个原因，我自己还是感觉构造的数据质量比较低影响了。也可能是我的测试数据构造的太少。&lt;/p&gt;

&lt;h2 id=&quot;rl&quot;&gt;RL&lt;/h2&gt;

&lt;p&gt;后面我开始尝试 RL，用 GRPO 的方式来训练。verl 来运行 grpo 的训练倒是很简单，只是超参数配了有 50 多个。不过大部分参数都可以从 examples 或者 recipe 里直接抄。&lt;/p&gt;

&lt;p&gt;值得注意的是 verl 有很多关于 fsdp 的参数设置，其中又是需要区分是 actor 还是 ref model 的。比如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor_rollout_ref.actor.fsdp_config.param_offload&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor_rollout_ref.actor.fsdp_config.optimizer_offload&lt;/code&gt; 这两个参数，还有类似的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor_rollout_ref.ref.fsdp_config.param_offload&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor_rollout_ref.ref.fsdp_config.optimizer_offload&lt;/code&gt;，如果资源不够的话，这些参数都需要设置成 True 来开启 CPU offload 来节省显存。但是如果相对充足，那就可以把 actor 的 offload 关掉来提升训练效率，因为 actor 是需要频繁更新参数的，而 ref model 可以开着减少显存占用。&lt;/p&gt;

&lt;p&gt;大部分 examples 和 recipe 里都是这样做的，如果要调整的话尽量看一下不要错误地把 actor 和 ref 的参数搞混了，我有一次就把 actor offload 打开了，结果训练效率非常低，后来才发现这个问题。&lt;/p&gt;

&lt;p&gt;第一次训练的时候发现 reward_std 很低，frac_reward_zero_std 很高，说明几乎所有回答质量相同，模型不学习。我又开始研究怎么样生成好的数据。修改后好了一点，但是组内数据的方差还是不够大。于是我去请教 GPT 老师，它教给了我很多方法。&lt;/p&gt;

&lt;p&gt;比如 NGRPO（虚拟满分样本）​，在计算组内统计量时，添加一个虚拟的满分样本作为参照点。这样可以强行拉大 reward 的方差，能把训练继续下去。还有 WDB-GRPO（加权动态基线）​，移除标准差归一化（Dr.GRPO）​等等。&lt;/p&gt;

&lt;p&gt;最后我决定实现一下 NGRPO，因为它相对来说比较简单。AI 老师帮我写了代码，我又对照着 &lt;a href=&quot;https://github.com/nangongrui-ngr/NGRPO/blob/ngr/verl/trainer/ppo/core_algos.py#L156&quot;&gt;nangongrui-ngr/NGRPO&lt;/a&gt; 这个开源的 verl NGRPO 实现检查了一遍，发现 AI 老师写的是又快又好。&lt;/p&gt;

&lt;p&gt;后面的训练效果也确实好了很多，训练好的模型在测试集上的表现提高到了 85%，跟 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-chat&lt;/code&gt; 的表现差不多了，而它只是个 7B 小模型。&lt;/p&gt;

&lt;p&gt;到这里就差不多了，放假几天根本没有停下来，完全沉浸在这个炼丹实验中，虽然这个实验还很初级，数据构造也比较粗糙，但至少让我对后训练的过程有了一个完整的体验。&lt;/p&gt;

&lt;p&gt;最近对什么事情都有点提不起兴趣，也是趁这个机会让自己忙碌起来，记录一下这段时间的断断续续的探索。verl 和 slime 这样的框架真的挺不错的，让 RL 这个门槛挺高的事情变得相对容易了很多，配合 GPT 老师的指导，能够让人快速地开始一些实验。最近我看到 Thinking Machines 的产品 &lt;a href=&quot;https://tinker-docs.thinkingmachines.ai/&quot;&gt;tinker&lt;/a&gt;，也是希望降低后训练的门槛，还是蛮有趣的。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Tinker lets you focus on what matters in LLM fine-tuning – your data and algorithms – while we handle the heavy lifting of distributed training.&lt;/p&gt;

  &lt;p&gt;You write a simple loop that runs on your CPU-only machine, including the data or environment and the loss function. We figure out how to make the training work on a bunch of GPUs, doing the exact computation you specified, efficiently. To change the model you’re working with, you only need to change a single string in your code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 16 Feb 2026 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/jiucai-rl</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/jiucai-rl</guid>
      </item>
    
      <item>
        <title>2025 年终总结：当时只道是寻常</title>
        <description>&lt;p&gt;由于创业的缘故，2025 年过得特别快，我也没有太多时间去记录和反思。同样地，能够写一些自己感兴趣的代码的时间也变少了不少。随手记录一些印象比较深刻的事情吧，前面可能偏向技术，后面可能会偏向生活。比较杂乱，姑且一读。&lt;/p&gt;

&lt;p&gt;一月份 DeepSeek 像一道闪电划过天际，我也被深深地震撼到了。趁着春节假期，我部署了一个小参数的版本在自己的服务器上玩了一阵子，发现 vLLM 对于 DeepSeek 的支持在那时还不是特别好。在 DeepSeek 之前的开源模型，都不支持 reasoning 的能力，因此 vLLM 并不支持 &lt;a href=&quot;https://docs.vllm.ai/en/latest/features/reasoning_outputs/&quot;&gt;reasoning outputs&lt;/a&gt;，因此使用起来非常不方便，需要手动区分推理和最终输出部分，在流式输出的时候更是如此。&lt;/p&gt;

&lt;p&gt;因此我花了一些时间，给 vLLM 提交了 PR &lt;a href=&quot;https://github.com/vllm-project/vllm/pull/12473&quot;&gt;[Frontend] Support reasoning content for deepseek r1&lt;/a&gt;。因为本身这一功能涉及到 streaming，function calling 等不同的推理特性，最开始实现的时候代码交织在了一起。为了确保未来其他 reasoning 模型也能复用，参考 vLLM 那时的 function calling parser 的设计，实现了一个比较通用的 reasoning parser。因此改动比较大，加上文档一共有 1000 行左右。但那时 vLLM 的社区非常活跃，经过了几轮的 review 和修改，最终 PR 在一周内就被合并了，现在 vLLM 到了 v1 版本也仍然在沿用这个设计。&lt;/p&gt;

&lt;p&gt;虽然我一直不是特别看好模型推理的市场以及就业前景，但作为一个工程师来说，性能优化和系统设计的挑战还是非常有趣的。后续我也继续围绕 vLLM 继续参与了一些工作。比如 &lt;a href=&quot;https://github.com/vllm-project/production-stack&quot;&gt;vllm-production-stack&lt;/a&gt;，基于 LMCache 和 vLLM 在 Kubernetes 上部署一个可扩展的推理服务，能够支持 request routing，kv offloading 等特性。&lt;/p&gt;

&lt;p&gt;不过最让我感兴趣的是 &lt;a href=&quot;https://github.com/vllm-project/vllm/issues/12511&quot;&gt;[Feature]: Support torch.distributed as the runtime for multi-node inference&lt;/a&gt;。本身 vLLM 的分布式推理需要依赖 Ray，Ray 虽然确实是一个很好的分布式计算框架，但对于部署非常静态的模型推理服务来说，使用 Ray 会增加不少复杂度。相比之下，PyTorch 自带的分布式计算功能就要简单很多，尤其是在现代生产环境普遍使用 Kubernetes 的情况下，使用 torch.distributed 作为分布式推理的后端会更加方便。我花了一些时间实现了一个原型，脱离了 Ray 后分布式推理就非常简洁：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# single node, multi-gpu
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;torchrun&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nproc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;per&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vllm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entrypoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;openai&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api_server&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# multi node, on node 0
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;torchrun&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nnodes&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nproc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;per&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rdzv_backend&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c10d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rdzv_endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node_0_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vllm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entrypoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;openai&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api_server&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# multi node, on node 1
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;torchrun&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nnodes&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nproc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;per&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rdzv_backend&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c10d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rdzv_endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node_0_ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;python&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vllm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entrypoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;openai&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api_server&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;类似这样的命令就可以启动一个多节点的分布式推理服务了，这跟 SGLang 的设计很像。不过我观察到 SGLang 社区也有 issue 希望为 SGLang 增加 Ray 的分布式支持 &lt;a href=&quot;https://github.com/sgl-project/sglang/issues/4678&quot;&gt;[Feature] Support Ray Backend for distributed inference&lt;/a&gt;，我自己还是持保留意见。如果要考虑上层调度的兼容性，那我认为 vLLM 支持 torch.distributed，调度完全交给 Kubernetes 会是一个更好的选择。Application 层面的调度也交给 Kubernetes 来做，会拥有一个更加统一的调度视角。&lt;/p&gt;

&lt;p&gt;说到这个，我觉得 Kubernetes 已经事实意义上被 Mesos 的双层调度设计夺舍了。Kubernetes 的 CRD operator 和自定义调度器，已经成了支持不同工作负载不可或缺的一部份。事实证明 One for all 是很难做到的。&lt;/p&gt;

&lt;p&gt;重新说回 AI，仅在今年，美国科技巨头在AI基础设施上的资本支出就已达到惊人的 3000 亿美元——这相当于美国全年关税收入的 1.5 倍。对于美国而言，AI 似乎已成一个 “All in” 的战略方向。在股市中，英伟达、苹果、微软、谷歌、Amazon 与 Meta 这六家公司的总市值，已占据美股的 30% 以上。&lt;/p&gt;

&lt;p&gt;许多分析都从资本支出的角度，预言 AI 在 2026 年将继续高歌猛进。但资本支出是一个滞后指标。数据中心的建设必然早于模型的预训练，而预训练水平的提升虽受益于算力基建，却并非简单的线性关系。即便预训练技术进步停滞，资本支出也可能因建设周期而惯性延续。&lt;/p&gt;

&lt;p&gt;那么，大语言模型的预训练是否正在触及瓶颈？关于这一点，我与 &lt;a href=&quot;https://www.zhihu.com/question/1974931646080836522/answer/1985650663905055367&quot;&gt;Yibo Zhu 在知乎的回答中的看法&lt;/a&gt;相似。感兴趣的朋友可以去看看。&lt;/p&gt;

&lt;p&gt;最后聊聊 agent infra，人人都在谈论 agent，但到底什么是 agent 需要的基础设施？Agent 是不是需要新的基础设施？我觉得目前还没有一个清晰的答案。回顾软件工程的历史，基础设施的变革往往源于需求的巨大变化，但是需求的巨大变化并不一定会带来基础设施的变革。传统互联网的业务复杂度提升带来了分布式系统的兴起，使得单体应用逐渐被微服务取代，从而诞生出了分布式数据库事务，分布式一致性协议，服务网格等一系列新的基础设施。而新的线上线下一体化业务场景，比如饿了么、美团等，在需求和模式上发生了巨大变化，但是基础设施的技术上并没有发生根本性的变革，只是上层业务形态变得更加复杂了。&lt;/p&gt;

&lt;p&gt;Agent 可能存在一些安全，可观测性等方面的需求，但是这些需求并没有达到需要基础设施变革的程度。绝大多数 AI 在不同场景的应用，只是稍微往计算密集的工作负载方向偏移了一些，比寻常的业务多需要一些 GPU，并没有带来基础设施的变革，仍然是一个个独立的服务在运行着。不少 agent infra 提到的 state management，tool use 等等，也都是在应用层面进行的设计，并没有触及基础设施层面。到最后你依然需要的是跟之前一样的集群调度系统，虚拟化技术等等。我能看到的 AI infra 的机会仍然只有计算和存储，具象化到产品就是硬件层面的加速卡，芯片，和软件层面的推理服务产品和数据库/搜索引擎。&lt;/p&gt;

&lt;p&gt;最后分享一下生活。今年 3 月份我和小徐去日本旅游了几天，距离我们上次旅游已经要四五年了。这次我们去了大阪，工作日的时候我在远程办公，周末的时候我们一起逛了道頓堀，天守阁，还有几个并不那么热门的临海的小地方，临空城什么的。大阪是一个人山人海的城市，我从没有见过这么多游客。尤其是道頓堀，简直就是人挤人的节奏。感觉整个大阪都被游客给占领了。这种地方对于我来说，呆几天体验一下还不错，但如果长期生活的话，估计会被折磨死。&lt;/p&gt;

&lt;p&gt;大阪的旅游资源我自己觉得不是特别多，其中著名的天守阁游客非常多，进去需要脱鞋，而我和小徐没有事先调研，去的时候没穿袜子，光脚走在上面还挺冷的。虽然天守阁里面不是非常有意思，但是外面的景色跟刺客信条影里的风景极其相似。小徐自己在工作日还去了奈良，见到了会鞠躬的鹿。欲望不仅会规训人类，也规训动物。&lt;/p&gt;

&lt;p&gt;为了办理美国签证，我和小徐还在周末去了一趟武汉。武汉呆的时间不久，给我印象最深的是黄鹤楼和藕汤。黄鹤楼的风景确实不错，登高望远，江城尽收眼底，就是仍然是熟悉的人山人海。藕汤倒是让我印象深刻。我本来以为藕汤就是简单的莲藕做成汤，以为没有多少。我和小徐去店里点了不少菜，上菜后发现藕汤份量极大，还有大量的肉骨，很好吃但量实在太大。在上海被小份量的饮食和超高物价洗脑太久，以至于吃完结账的时候我还在认真思考是老板做慈善还是我在上海做冤大头做久了脑子坏掉了。&lt;/p&gt;

&lt;p&gt;下半年的时候我们去了美国加州呆了一段时间，多亏是跟 Allen 一起去的，不然我们是无法生存的。湾区的风景确实非常棒，海景和山景交相辉映，非常漂亮。不过生活成本实在太高了，吃饭和住宿的费用都非常惊人。去了后除了工作外，还见了不少多年未见的朋友，下次见面不知道是什么时候了。&lt;/p&gt;

&lt;p&gt;今年我玩的游戏不是特别多，没有什么印象特别深的作品。33 号远征队创历史的年度最佳奖项记录，人情世故。丝之歌的正反馈直到我退游也没感受到。不过今年 10 月我捡起了去年 12 月就发布的燕云十六声。刚发布时候我感觉手感好差，再加上那时候有很多游戏可以玩，很快就弃坑了。今年趁着十一假期，我重新玩了一下，确实给了我很多惊喜。虽然还是有网游的味道，但整体的剧情和玩法设计都非常不错。这是为数不多让我觉得玩得下去的 MMO 游戏，推荐给大家。&lt;/p&gt;

&lt;p&gt;写这些时翻了翻相册，看到半年前去世的猫，忽然就想起“当时只道是寻常”这句话。当时觉得普通的日子，现在回看却那么珍贵。一度不想写下去，但坚持了十年的习惯，也不想断在这里。就简单记下这些吧。希望读到这篇文章的朋友和我自己都能够珍惜眼前。&lt;/p&gt;

&lt;h2 id=&quot;往年总结&quot;&gt;往年总结&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2024&quot;&gt;2024&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2023&quot;&gt;2023&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2022&quot;&gt;2022&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2021&quot;&gt;2021&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2020&quot;&gt;2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2019&quot;&gt;2019&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2018&quot;&gt;2018&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2017&quot;&gt;2017&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2016&quot;&gt;2016&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2015&quot;&gt;2015&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/record&quot;&gt;2014&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 30 Dec 2025 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/newyear2025</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/newyear2025</guid>
      </item>
    
      <item>
        <title>通用 AI Agent 真的有未来么？从 RSS 谈起</title>
        <description>&lt;blockquote&gt;
  &lt;p&gt;此文与 Agent 技术毫不相干，只是个人对 Agent 未来商业模式的一些思考。工程师朋友们可以直接跳过。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最近跟同事聊天时候，聊到了 Folo 的一些近况，进而延伸到对 RSS 的讨论。我认为 RSS 的兴衰对 Agent 是非常有历史借鉴意义的，本文就来聊聊这个话题。&lt;/p&gt;

&lt;p&gt;首先给不太了解 RSS 的读者简单介绍一下它。RSS（Really Simple Syndication）中文译作简易信息聚合，也称聚合内容，是一种消息来源格式规范，用以聚合多个网站更新的内容并自动通知网站订阅者。使用RSS后，网站订阅者无需手动查看网站是否有新的内容，同时RSS能将多个网站的更新集成并以摘要形式呈现，有助于订阅者快速获取重要信息，并选择性点击查看。&lt;/p&gt;

&lt;p&gt;举个例子来说，现在我们刷小红书，需要打开小红书 App，查看不同的内容推荐。而如果有一个 RSS 阅读器，我们就可以把小红书、微博、知乎等多个内容源的更新都聚合到一个地方，统一查看和管理。&lt;/p&gt;

&lt;p&gt;相比于现在各个应用独立运营、各自为政的内容分发模式，RSS 的优势在于它的开放性和统一性。用户可以自由选择订阅哪些内容源，而不必受限于某个应用的推荐算法或商业策略。&lt;/p&gt;

&lt;p&gt;随着互联网的发展，RSS 非常不出人意料地迎来了衰落，甚至死亡。它的诞生是非常理想主义的，建立在一个开放的，利它的互联网的基础上。但是现在，全球市值最高的公司都是封闭生态系统的代表。这些公司通过控制内容分发渠道，来实现商业利益最大化。而 RSS 的开放性和去中心化特性，显然与这些公司的商业模式相冲突。&lt;/p&gt;

&lt;p&gt;RSS 使得内容提供者难以通过广告和数据变现，因为用户可以直接通过 RSS 阅读器获取内容，而不需要访问内容提供者的网站。这削弱了内容提供者对用户的控制力，进而影响了他们的收入来源。它是一种规避广告、规避点击收入的方式。内容的消费者和消费者并没有都从中获益，内容提供者反而受损。这样的模式注定难以在商业环境中生存。&lt;/p&gt;

&lt;p&gt;那么，AI Agent 呢？它会不会重蹈 RSS 的覆辙？目前 Agent 的概念仍然非常模糊，大家对它的理解也不尽相同。我自己倾向于认为只有 Comet / Manus 这种能够自主决策、执行任务的 Agent 才是真正意义上的 Agent。而垂直领域的工具型产品，比如说 Cursor 等，只是传统 SaaS 产品的升级版，并不是提升人类通用任务的自动化水平的 Agent。&lt;/p&gt;

&lt;p&gt;我相信这样的垂直领域的产品是非常有商业价值的，但是它们并不能代表 Agent 的未来。真正的 Agent 应该是能够跨领域、跨任务，帮助用户完成各种复杂任务的智能体。垂直领域的产品并不需要商业模式的创新，它们可以继续沿用传统的 SaaS 收费模式。比如帮助程序员写代码的工具，可以按月订阅收费；帮助设计师生成图片的工具，也可以按使用量收费。这些工具型产品的商业模式已经被验证是可行的。&lt;/p&gt;

&lt;p&gt;如何区分通用 Agent 和垂直领域的工具型产品？我认为关键在于它们是否能够自主决策、执行任务。垂直领域的产品的需求是明确的，用户知道自己需要什么，产品只需要提供相应的功能即可。而通用 Agent 则需要理解用户的意图，制定计划，并自主执行任务。这需要更高水平的智能和自主性。比如 Cursor，用户的需求就是帮助它写代码。而如 Comet 这样的浏览器中的通用代理，可以帮助用户访问网站，完成自定义的任务。它的复杂度要远大于 Cursor。通用 Agent 是不可能通过收取订阅费用来盈利的，它本身是非常强势的用户入口，影响了太多相关方的利益，现有的商业模式不具备可行性。&lt;/p&gt;

&lt;p&gt;通用 Agent 就是让 AI 真的像人一样，利用各种工具和资源，去完成用户交代的任务。它需要具备理解能力、计划能力和执行能力。这样的 Agent 才有可能真正提升人类的生产力，解放人类的时间。但是，它一定会面临跟 RSS 一样的问题。RSS 的设计里，阅读器是内容的入口，而内容提供者则失去了对用户的控制权。Agent 产品也是如此，用户通过 Agent 来获取信息和服务，内容提供者同样会失去对用户的控制权。&lt;/p&gt;

&lt;p&gt;最近&lt;a href=&quot;https://www.reuters.com/business/retail-consumer/perplexity-receives-legal-threat-amazon-over-agentic-ai-shopping-tool-2025-11-04/&quot;&gt;亚马逊起诉 Perplexity &lt;/a&gt;很好地说明了这个问题。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;亞馬遜指出，Perplexity 的 Comet 代理在執行購物任務時，並未告知平台其為「代用戶行動的 AI」，違反了網站禁止「資料挖掘與自動化工具」的條款。亞馬遜於訴狀中指控該行為構成「電腦詐欺」，強調這種未經許可的行為「就像闖入者撬鎖進門，只是這次用的是程式碼」。&lt;/p&gt;

  &lt;p&gt;而 Perplexity 則回應，亞馬遜的動作是「惡霸行為」，目的是阻止創新、封殺競爭，讓用戶無法自由選擇自己偏好的 AI 助手。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不管你是否支持开放的互联网，都必须承认互联网就是在变得越来越封闭。让 Agent 像人一样去完成任务，但 Agent 毕竟不是人。人会看广告，会点击链接，会购买产品。而 Agent 则会规避这些行为，因为它们的目标是高效完成任务，而不是被广告干扰。&lt;/p&gt;

&lt;p&gt;而大公司为什么会允许别人成为入口？我不认为在技术上通用的 Agent 是不可能实现的，我对此非常乐观。但是我对于 Agent 的订阅制商业模式充满质疑。市场和亚马逊这样的公司必然不可能接受。如果没有商业模式的创新，是必然不可能打破现状。&lt;/p&gt;

&lt;p&gt;之前 Perplexity CEO 去年在一个 &lt;a href=&quot;https://lexfridman.com/aravind-srinivas-transcript/&quot;&gt;podcast&lt;/a&gt; 里有分享过跟谷歌的竞争的一些看法，我听下来的感觉就是认可了谷歌在竞价广告和搜索上的优势。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We would rather take a more dramatic position, that the best way to actually make a dent in the search space is to not try to do what Google does, but try to do something they don’t want to do. For them to do this for every single query is a lot of money to be spent, because their search volume is so much higher.&lt;/p&gt;

  &lt;p&gt;我们更倾向于采取一种更为激进的策略，即真正撼动搜索领域的最佳途径并非效仿谷歌的做法，而是尝试去做他们不想做的事情。对他们而言，如果每次搜索查询都这样做，将会耗费巨资，因为他们的搜索量远高于我们。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在今年 4 月一个 TechCrunch 的采访中，Perplexity CEO 也提到过未来希望通过浏览器来收集用户数据，卖给用户更好的广告：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“That’s kind of one of the other reasons we wanted to build a browser, is we want to get data even outside the app to better understand you,” Srinivas said. “Because some of the prompts that people do in these AIs is purely work-related. It’s not like that’s personal.”&lt;/p&gt;

  &lt;p&gt;“这也是我们想开发浏览器的原因之一，我们希望获取应用之外的数据，以便更好地了解用户，”Srinivas说道。“因为用户在这些人工智能应用中的一些操作纯粹与工作相关，并不涉及个人隐私。”&lt;/p&gt;

  &lt;p&gt;“On the other hand, what are the things you’re buying; which hotels are you going [to]; which restaurants are you going to; what are you spending time browsing, tells us so much more about you,” he explained.&lt;/p&gt;

  &lt;p&gt;“另一方面，你购买了什么商品？你入住了哪些酒店？你去了哪些餐厅？你浏览了哪些内容？这些信息能让我们更深入地了解你，”他解释道。&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;Srinivas believes that Perplexity’s browser users will be fine with such tracking because the ads should be more relevant to them.&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;Srinivas认为Perplexity的浏览器用户不会介意这种追踪，因为广告会与他们更加相关。&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;“We plan to use all the context to build a better user profile and, maybe you know, through our discover feed we could show some ads there,” he said.&lt;/p&gt;

  &lt;p&gt;他说：“我们计划利用所有相关信息来构建更好的用户画像，或许我们还可以通过我们的发现信息流在那里展示一些广告。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;基本来说，目前 Agent 的商业模式仍然还是广告。后来者通过广告成为新的互联网巨头，然后再通过封闭生态系统来实现商业利益最大化的故事也并不是没有，比如 TikTok 就是一个很好的例子。但是它和 Agent/RSS 之流有本质的区别。TikTok 本身就拥有内容。它并不是一个中间人，而是内容的提供者。Agent 产品则不同，它是一个中间人，连接用户和内容提供者。这样的中间人角色注定会面临来自内容提供者的阻力。&lt;/p&gt;

&lt;p&gt;如果仍然是卖广告，为什么谷歌不做 Agent 呢？虽然现在 Chrome 在进行反垄断的闯关中，但是这一个市场没有任何道理会被谷歌错过。我个人认为，除非 Agent 能够找到一种全新的商业模式，否则它注定会像 RSS 一样，成为一个理想主义的产物，而不是一个商业上成功的产品。市场仍然属于谷歌。&lt;/p&gt;

&lt;p&gt;那么，有没有可能出现一种新的商业模式，既能让 Agent 成为用户的入口，又能让内容提供者和平台方都能接受呢？回顾历史已经不能解答这样的问题了，可能需要当初谷歌改进提出竞价广告模式那样的创新才能解决这个问题。最近我也一直在思考，如果有新的想法，我会再写一篇文章来分享。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 05 Dec 2025 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/agent-model</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/agent-model</guid>
      </item>
    
      <item>
        <title>纪念我家的宠物猫 env</title>
        <description>&lt;blockquote&gt;
  &lt;p&gt;我家的第一只宠物猫于 2025 年 5 月 31 日去世，谨以此文纪念他。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Env 在 2021 年出生，2022 年他来到了我家。小徐一直想养一只猫，自我 2019 年毕业到 2021 年之间我们都住在上海的出租房里，不便饲养。直到我们搬家安顿好后，养猫才真正提上日程。那时我对猫既不了解也不感兴趣，所以品种完全由小徐决定。现在回想起来，我更喜欢德文或阿比西尼亚那种干练的类型，不过当时小徐选定了可爱的蓝金渐层长毛猫。&lt;/p&gt;

&lt;p&gt;Env 来我家时，我​​正​​刚从腾讯离职创业。我们便用第一个创业项目的名字 – &lt;a href=&quot;https://github.com/tensorchord/envd&quot;&gt;envd&lt;/a&gt; – 给他命名。初来乍到的他​​特别小，两只手就能轻轻捧住。刚到新的环境并不适应，喜欢像一条小毛毛虫般蜷缩着躲在桌下​​。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env.jpg&quot; alt=&quot;env&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;刚到家时蜷缩在桌子下&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-1.jpg&quot; alt=&quot;env&quot; height=&quot;600&quot; width=&quot;400&quot; /&gt;
    &lt;figcaption&gt;另外一个蜷缩位置&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;我仍然记得他刚到家的第一晚。我们不让他进卧室，他就在门外叫了​​整宿​​。​​那样小小的身子，竟能爆发出如此持久的能量，真是让人又爱又怜​​。​​最初​​，那奶声奶气的叫声确实萌化人心，​​可​​听久了也难免有些心烦。​​好在​​没过多久，​​他就接受了自己晚上不能进卧室这件事​​，不再整夜喵喵喵地​​唤门​​。&lt;/p&gt;

&lt;p&gt;那时小徐刚做完脚部手术，在家休养。Env ​​​非常​​温顺，每天早上起来都会主动地用头蹭我们的腿，走到哪里跟到哪里，对着我们撒娇希望我们摸摸他的头。每当我出门回来后，也会竖着尾巴跑到门口迎接我。我接触过不少猫，无论是朋友家的还是宠物店的，但从未遇到​​性格像 Env ​​这么温​​和​​贴心​​的。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-2.jpg&quot; alt=&quot;env&quot; height=&quot;500&quot; width=&quot;800&quot; /&gt;
    &lt;figcaption&gt;陪小徐看手机&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;因为我们是第一次养猫缺乏经验，接他回家时我们也没懂得检查健康情况​​。Env 从猫舍被送来时带着一身的病。不仅有耳螨，而且还有眼睛的感染。全赖​​小徐悉心照料了​​好一阵子​​，他才​​慢慢​​康复。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-3.jpg&quot; alt=&quot;env&quot; height=&quot;400&quot; width=&quot;600&quot; /&gt;
    &lt;figcaption&gt;可爱的摆拍&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;等他稍长大些，我们便早早带他做了​​绝育​​手术。​​不知是先天发育问题还是我们照料疏忽，Env出现了隐睾，为此多挨了一刀​​。术后他​​一度郁郁寡欢​​，但很快​​又恢复了活泼。​​那阵子我们常带他出门散步。​​虽说英国长毛猫本不宜外遛​​，但我们总​​挑些僻静处陪他探索​​。​可能是初生牛犊不怕虎，小时候的他在室外环境里并没有很多害怕，反而对周围充满了好奇。一边贴着墙角小心翼翼地走着猫步，一边对着陌生的环境到处嗅嗅闻闻，非常可爱。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-out.jpg&quot; alt=&quot;env&quot; height=&quot;400&quot; width=&quot;600&quot; /&gt;
    &lt;figcaption&gt;外出的 Env&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;等他再长大些，能轻松跳上我的桌子时，便常常来陪我办公。起初，他总爱​​把脑袋往我手里拱​​，​​可一见​​我​​准备​​敲键盘，就​​识趣地​​蜷​​在键盘和显示器之间的空当里​​——​​不是呼呼大睡，就是盯着屏幕出神​​。​​无奈我桌面空间本就不大，只得经常把他抱下去。​​久而久之，他大概明白这里并不欢迎“陪工”，便不再上桌，转而伏在窗台或阳台上，对着外面的风景若有所思了。​&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-keyboard.jpg&quot; alt=&quot;env&quot; height=&quot;500&quot; width=&quot;800&quot; /&gt;
    &lt;figcaption&gt;键盘旁的 Env&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Env ​​在家的第一个冬天，被迫穿上了我们准备的衣服​​。他极不喜欢身上有束缚，总是想方设法​​扒拉下来​​。可上海的冬天湿冷刺骨，我们担心他着凉，只能​​一次一次地给他穿回去​​。这场拉锯战​​反​​让我想起小时候被父母追着添衣的情景。除了穿衣，他还尤其抗拒刷牙。每次刷牙都如临大敌，四爪乱蹬拼命挣扎。​​小徐为此和他斗智斗勇，​​场面常令人忍俊不禁。​​&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/env-cat/env-4.jpg&quot; alt=&quot;env&quot; height=&quot;400&quot; width=&quot;600&quot; /&gt;
    &lt;figcaption&gt;逃避刷牙&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;照顾 Env 自然不是一帆风顺。作为长毛猫，​​肠胃不适时他屁股常沾污物，免不得我们得仔细清理​​。偶尔他会​​在凌晨三四点​​叫得​​人睡不安稳​​。心爱的窗帘也被他​​扒拉得面目全非​​。日常的铲屎、换水、清洁食盆饮水机更是​​从不间断​​。​​可当​​他离开以后，​​我能记住的都是他带给我们的欢乐。&lt;/p&gt;

&lt;p&gt;他的离世我不愿意再回想一次，只希望他在猫生的四年里是快乐开心的，希望他走的时候没有痛苦。希望他能时常出现在我的梦里。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 03 Jun 2025 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/env-cat</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/env-cat</guid>
      </item>
    
      <item>
        <title>在 Kubernetes 中 Autoscale LLM 的实践</title>
        <description>&lt;p&gt;在 2023 年的时候，我们实现了一个大型语言模型（LLM）的无服务器推理平台产品 &lt;a href=&quot;https://modelz.ai&quot;&gt;ModelZ&lt;/a&gt;。我们还通过 &lt;a href=&quot;https://github.com/tensorchord/openmodelz&quot;&gt;OpenModelZ&lt;/a&gt; 开源了一些核心组件。在 Kubernetes 上部署 LLMs 与传统的机器学习模型部署相比有一些新的变化和挑战。虽然我们现在的重点已经转移到基于 PostgreSQL 的向量数据库 &lt;a href=&quot;https://vectorchord.ai&quot;&gt;VectorChord&lt;/a&gt;，但我们希望分享我们在构建产品时的经验，希望对社区中的其他人有所帮助。&lt;/p&gt;

&lt;p&gt;本文将涵盖以下主题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;动机&lt;/li&gt;
  &lt;li&gt;系统的目标和非目标&lt;/li&gt;
  &lt;li&gt;架构和权衡
    &lt;ul&gt;
      &lt;li&gt;容器加载和缓存&lt;/li&gt;
      &lt;li&gt;负载均衡&lt;/li&gt;
      &lt;li&gt;调度和自动扩展&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;与其他实现方式的比较&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;动机&quot;&gt;动机&lt;/h2&gt;

&lt;p&gt;回到 2023 年，受到 OpenAI 的 ChatGPT 的成功的启发。我们认为开源的 LLMs 是 AI 的未来，由此产生了非常多的部署需求。而满足这一需求最直观的方式是提供一个平台，开发者可以在上面部署他们的 LLMs，并通过一个简单的 API 来访问。或者你可以说，我们想要构建一个 serverless LLM 推理服务。&lt;/p&gt;

&lt;p&gt;用户上传模型文件。当请求到来时，模型推理服务会被运行，然后返回结果。这样的服务可以在需要时自动扩展，而不需要用户关心底层的基础设施。值得一提的是，这样的产品形态与 &lt;a href=&quot;https://www.together.ai/products#inference&quot;&gt;Together.AI&lt;/a&gt; 或 &lt;a href=&quot;https://fireworks.ai/&quot;&gt;Fireworks&lt;/a&gt; 有所不同。它们的计费单位是 Token，而我们的计费单位是 GPU 时间。因此更像是 &lt;a href=&quot;https://modal.com/&quot;&gt;Modal&lt;/a&gt;、&lt;a href=&quot;https://replicate.com&quot;&gt;Replicate&lt;/a&gt; 和已经 sunset 的 &lt;a href=&quot;https://banana.dev/&quot;&gt;Banana.dev&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;现在看来，这个产品形态是有一些问题的。以 Token 为计费单位的产品能够通过超卖等方式来提高利润，而以 GPU 时间为计费单位的产品则需要更多的成本。或许 LLM as a service 而非 LLM inference as a service 的产品有更好的商业模式。但与本文的主题无关，就不再讨论。&lt;/p&gt;

&lt;h2 id=&quot;使用流程&quot;&gt;使用流程&lt;/h2&gt;

&lt;p&gt;在介绍技术细节之前，我们先来看看用户使用产品的流程。用户可以选定一个模型，并以此创建推理服务。在使用之前，需要设定一些用于自动扩缩容的参数，如：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min-replicas&lt;/code&gt;：最小的副本数。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-replicas&lt;/code&gt;：最大的副本数。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target-qps&lt;/code&gt;：每个副本的理想并发请求数。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scale-to-zero-duration&lt;/code&gt;：空闲多久后扩缩容到 0。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startup-duration&lt;/code&gt;：等待部署启动的时间。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;随后用户会得到一个与 OpenAI API 兼容的 endpoint，可以通过这个 endpoint 来访问推理服务。&lt;/p&gt;

&lt;h2 id=&quot;架构&quot;&gt;架构&lt;/h2&gt;

&lt;p&gt;我们遵循了一个中心化的设计，在控制面中有多个组件，下图展示了这些组件之间的关系。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/modelz/arch.png&quot; alt=&quot;Architecture&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Architecture&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Gateway 是用户请求的入口，它会将请求转发给对应的推理服务。Kubernetes Controller 负责管理推理服务的生命周期，根据用户选择的不同框架和模型创建对应的 Deployment，与此同时，可能会设置一些环境变量配合其他组件对冷启动进行加速，后续会详细介绍。Autoscaler 负责根据用户设置的扩缩容策略与当前的负载情况来调整副本数。&lt;/p&gt;

&lt;p&gt;架构是简洁的，&lt;a href=&quot;https://docs.skypilot.co/en/latest/serving/sky-serve.html#skyserve-architecture&quot;&gt;SkyPilot Serve&lt;/a&gt; 与我们当初的设计有很多相似之处。但是仅仅如此是不够的，我们还需要考虑一些细节问题。&lt;/p&gt;

&lt;h2 id=&quot;冷启动&quot;&gt;冷启动&lt;/h2&gt;

&lt;p&gt;最突出的问题是如何加速冷启动。这也是 LLM 为代表的新型模型部署的一个共性问题。在传统的模型部署中，模型的加载时间是可以接受的，但是在 LLM 的场景下，模型参数从 10B-500B 不等，DeepSeek v3 更是将模型推到了 670B 的规模。在运行时，推理服务需要首先加载模型参数，过大的模型参数会导致加载时间过长，用户体验不佳。因此，我们需要一些方法来加速这个过程。&lt;/p&gt;

&lt;p&gt;在冷启动的过程中，通常主要的延迟来源是镜像的加载和模型参数的加载两方面。镜像被 Kubelet 从镜像仓库拉取到本地，然后被加载到容器中。模型则是从远程存储下载到本地，然后加载到 GPU 中。一般而言模型和镜像不再采取上一代 AI 工程化中常见的打包在一起的做法，而是分开存储。因此为了简化问题，我们可以将这两个问题分开来讨论。&lt;/p&gt;

&lt;h3 id=&quot;模型加载&quot;&gt;模型加载&lt;/h3&gt;

&lt;p&gt;模型的加载是一个 I/O 密集型的任务。通常模型会存储在一个远程的存储中，如 Huggingface Hub。模型首先从远程存储下载到本地的硬盘中，然后再加载到 GPU 中。&lt;/p&gt;

&lt;p&gt;在这个过程中，需要尽可能避免任何的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cast&lt;/code&gt; 操作，以免增加内存的使用，这通常发生在没有使用正确的精度的情况下（参考 &lt;a href=&quot;https://github.com/huggingface/transformers/issues/28476&quot;&gt;huggingface/transformers#28476&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;分析整个过程，我们可以将其大致分为两个阶段：下载和加载。一个直观的优化是维护单个集群内模型的缓存，在避免重复下载的同时，加速集群内缓存的分发。我们观察到，大部分的模型都在 Huggingface Hub 上进行存储和分发。因此，我们可以通过在集群内部署一个 Hub 的缓存来加速模型的下载。这样，模型只需要下载一次，就可以在集群内部共享。而且这一实现最好是透明的，用户无需关心这一过程，也无需修改代码。&lt;/p&gt;

&lt;p&gt;通常来说，用户会使用如下代码来加载模型：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;transformers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AutoTokenizer&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;device&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;cuda&quot;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# the device to load the model onto
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_pretrained&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Qwen/Qwen2-7B-Instruct&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;device_map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;auto&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AutoTokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_pretrained&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Qwen/Qwen2-7B-Instruct&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在这个过程中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformers&lt;/code&gt; 实际上会调用 &lt;a href=&quot;https://huggingface.co/docs/huggingface_hub/index&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;huggingface_hub&lt;/code&gt; client lib&lt;/a&gt; 来完成模型下载的过程。在之前版本的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;huggingface_hub&lt;/code&gt; 中，我们可以设置一个环境变量 &lt;a href=&quot;https://huggingface.co/docs/huggingface_hub/v0.13.2/en/package_reference/environment_variables#hfendpoint&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HF_ENDPOINT&lt;/code&gt;&lt;/a&gt; 来指定 Hub 的地址。我们可以通过设置这个环境变量来指向集群内的缓存服务器。这样 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformers&lt;/code&gt; 会将所有对 Huggingface Hub 的请求转发到集群内的缓存服务器上。&lt;/p&gt;

&lt;p&gt;缓存服务的逻辑也非常简单，将模型下载以外的请求全部转发到 Huggingface Hub 的服务器 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://huggingface.co&lt;/code&gt; 上，而对于模型下载请求，先检查本地是否有缓存，如果没有则从 Huggingface Hub 下载，然后再转发给用户。&lt;/p&gt;

&lt;p&gt;缓存的目录结构可以参考 Huggingface Hub 的目录结构，是 Key-Value 的形式：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ls $HOME/.cache/huggingface/cache-server/
103bad8a83037abcdb9d24f2b21eba8792b33a5d
193490b58ef62739077262e833bf091c66c29488058681ac25cf7df3d8190974
19da7aaa4b880e59d56843f1fcb4dd9b599c28a1d9d9af7c1143057c8ffae9f1
1a02ee8abc93e840ffbcb2d68b66ccbcb74b3ab3
1a189f0be69d6106a48548e7626207dddd7042a418dbf372cefd05e0cdba61b6
1b134cded8eb78b184aefb8805b6b572f36fa77b255c483665dda931fa0130c5
2c2130b544c0c5a72d5d00da071ba130a9800fb2
452ff48e07a68bcebea2a57423332ca5fd5c5126
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中的文件名是 Huggingface Hub 的请求 Header 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Linked-Etag&lt;/code&gt; 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ETag&lt;/code&gt; 的值，这个值是 Huggingface Hub 返回的一个唯一标识符，用于标识模型的版本。这样，我们就可以通过这个值来判断是否需要重新下载模型。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl --location --head &apos;http://HF_ENDPOINT/runwayml/stable-diffusion-v1-5/resolve/39593d5650112b4cc580433f6b0435385882d819/safety_checker/model.safetensors&apos;
HTTP/1.1 200 OK
Accept-Ranges: bytes
Access-Control-Allow-Origin: https://huggingface.co
Access-Control-Expose-Headers: X-Repo-Commit,X-Request-Id,X-Error-Code,X-Error-Message,ETag,Link
Content-Length: 1127
Content-Type: text/plain; charset=utf-8
Date: Fri, 03 Mar 2023 06:58:34 GMT
Server: nginx
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin, Accept
X-Linked-Etag: &quot;9d6a233ff6fd5ccb9f76fd99618d73369c52dd3d8222376384d0e601911089e8&quot;
X-Linked-Size: 1215981830
X-Powered-By: huggingface-moon
X-Repo-Commit: 39593d5650112b4cc580433f6b0435385882d819
X-Request-Id: Root=1-64019a9a-2a1c8df34f329c2167b71ce2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中还有不少工程的细节问题，比如如何利用一致性哈希等方式保证缓存的一致性，但大致的原理就是如此。在目前的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;huggingface_hub&lt;/code&gt; 的版本中，这一环境变量已经被移除，但仍然有类似的方式。这一优化与其他优化都不冲突，可以同时使用。在模型的下载的 benchmark 过程中，这一优化已经可以打满内网带宽。&lt;/p&gt;

&lt;p&gt;下载可以通过缓存来优化，而加载可以通过 stream 的方式来优化。&lt;a href=&quot;https://github.com/coreweave/tensorizer&quot;&gt;tensorizor&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/run-ai/runai-model-streamer&quot;&gt;runai model streamer&lt;/a&gt; 能够从本地或者远程的对象存储中以流式的方式将 safetensors 格式的 tensors 加载到 GPU 显存里。在当时我们并没有使用这一优化，但它理论上可以&lt;a href=&quot;https://www.run.ai/blog/run-ai-model-streamer-performance-benchmarks&quot;&gt;显著提升加载速度&lt;/a&gt;，因为在加载的过程中通常无法打满硬盘的读带宽。&lt;/p&gt;

&lt;p&gt;这样的优化未来会成为模型推理的标准配置，因为随着 LLM 被越来越多的人使用，削峰填谷的能力会变得越来越重要。一个 8B 的模型在 safetensors load 的过程会花费 50s，而在 stream 的过程中只需要 15s。&lt;/p&gt;

&lt;h3 id=&quot;镜像加载&quot;&gt;镜像加载&lt;/h3&gt;

&lt;p&gt;在模型之前，镜像的加载也是一个重要的问题。由于包含了 CUDA 和 PyTorch 等框架的原因，镜像的大小通常会超过 10GB。在 Kubernetes 中，有非常多的方式来加速镜像的加载。我们使用了 &lt;a href=&quot;https://cloud.google.com/kubernetes-engine/docs/how-to/image-streaming&quot;&gt;GCP image steaming&lt;/a&gt; 来加速镜像的加载。因为这是闭源的实现，我们不清楚其具体的实现细节，但是经过测试与 Drgaonfly 等开源的实现相比，它的性能略好。&lt;/p&gt;

&lt;p&gt;关于这一问题，Modal 有一个很好的技术分享：&lt;a href=&quot;https://www.youtube.com/watch?v=SlkEW4C2kd4&amp;amp;ab_channel=NYCSystems&quot;&gt;Fast, lazy container loading in modal.com by Jonathon Belotti&lt;/a&gt;。他们使用了 Lazy pulling 的方式，并且对 FUSE 进行了非常多的调优与优化。GCP image streaming 根据实际效果，可能是一个类似的实现。&lt;/p&gt;

&lt;p&gt;蚂蚁金服的 Head of Conatiner Infra 王旭在 2020 年有一篇文章&lt;a href=&quot;https://mp.weixin.qq.com/s/9XjtT32_dgV3cMWUTlLY0Q&quot;&gt;镜像格式二十年的螺旋进化之路&lt;/a&gt;，里面提到对 OCIv2 的一些构想，但是很可惜 OCI image spec 一直停留在 v1.x。关于它的讨论也越来越少。对于大多数传统应用而言镜像加载的时间并不是一个很大的问题，但是对于 LLM 这样的模型推理服务而言，是非常值得优化的。理想情况下，镜像应该被 Hybrid eager and lazy 的方式被加载。在容器启动的时候只需要部分必要的内容，开始进行模型的下载，在下载的过程中应该打满网络带宽。模型下载完成，在加载模型时或后，镜像可以继续下载，这样可以全程充分利用 IO 资源。&lt;/p&gt;

&lt;h2 id=&quot;autoscaling&quot;&gt;Autoscaling&lt;/h2&gt;

&lt;p&gt;我们采取了与 &lt;a href=&quot;https://github.com/openfaas&quot;&gt;OpenFaaS&lt;/a&gt; 类似的系统设计以支持自动扩缩容的实现。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/modelz/lb.png&quot; alt=&quot;Autoscaling&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Autoscaling&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;用户所有的请求会经过 Gateway，由于存在服务被缩容到 0 实例的情况，所以可能会存在没有对应服务的情况。在这种情况下，Gateway 会经由 Kubernetes controller 扩容服务。因此 Gateway 为了性能考虑，维护了一个集群内服务状态的缓存，以避免频繁地与 Kubernetes API server 交互。&lt;/p&gt;

&lt;p&gt;除了从 0 到 1 的扩容工作由 Gateway 辅助完成，其他所有的扩缩容（从 1 到 多，从多到 1，从 1 到 0）工作都由 Autoscaler 负责。Autoscaler 会根据用户设置的参数和当前的负载情况来调整副本数。当前的负载情况，会由 Gateway 暴露的 Prometheus metrics 提供。&lt;/p&gt;

&lt;p&gt;由于我们的系统基于公有云，因此除了推理服务的 Autoscaling，还会涉及到集群节点的 Autoscaling。不同的云服务商都提供了各自的 cluster autoscaler 实现，但通常而言 GPU 节点的扩缩容花费的时间在 1-5 分钟之间。这意味着如果我们在没有空闲节点的情况下需要扩容，可能会有 5 分钟的延迟。为了避免这一问题，我们使用了 &lt;a href=&quot;https://github.com/kubernetes-sigs/cluster-proportional-autoscaler&quot;&gt;cluster-proportional-autoscaler&lt;/a&gt; 来维护一个预留的 GPU 节点池。这也是架构图中的 placeholder 实例的来源。cluster-proportional-autoscaler 会根据集群的节点数量，来创建对应数量的 pod，并且对应关系是可配置的。比如在集群节点数小于 3 的时候，可以只创建 1 个 pod。在节点数小于 16 的时候，创建 2 个 pod 等。通过 taint 和抢占调度的设计，这些 pod 可以被当作 placeholder 来使用，占住 GPU 资源，避免扩容时的延迟。当有了新的推理服务需要部署时，这些 pod 会被抢占，让出资源。&lt;/p&gt;

&lt;p&gt;在如此设计后，我们的集群是高度动态的，在节省成本的同时，也影响了镜像缓存的设计。被 cluster autoscaler 创建出的节点是没有任何镜像缓存的，因此在这些节点上部署服务时，需要重新下载所有镜像的所有层。为了解决这一问题，我们又引入了 &lt;a href=&quot;https://github.com/senthilrch/kube-fledged&quot;&gt;kube-fledged&lt;/a&gt; 来进行节点的公共基础镜像预热。它允许用户定义一组镜像列表，它会负责在部分选定的节点上缓存这些镜像。这样，当有新的节点被创建时，kube-fledged 会自动将这些镜像缓存到新的节点上。但是它存在一个 bug，使得 cluster autoscaler 创建的新节点&lt;a href=&quot;https://github.com/senthilrch/kube-fledged/issues/182&quot;&gt;加入集群后不会触发预热&lt;/a&gt;，为此我们维护了自己的 &lt;a href=&quot;https://github.com/tensorchord/kube-fledged&quot;&gt;fork&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;load-balancing&quot;&gt;Load Balancing&lt;/h2&gt;

&lt;p&gt;在 2023 年，还没有一个成熟的 LLM 的负载均衡方案。我们只是简单地使用 roundrobin 的方式在 Gateway 上进行负载均衡。因此 Gateway 既负责路由，又负责负载均衡。而 2024 年随着推理引擎上诸多 kvcache 优化的出现，对负载均衡也提出了新的挑战。&lt;/p&gt;

&lt;p&gt;kvcache 使得对话场景下的 LLM 推理服务不再是无状态的。针对这一问题，目前有两种思路。首先是 kvcache 的 live migration。目前我看到的工作有 &lt;a href=&quot;https://github.com/LMCache/LMCache&quot;&gt;LMCache&lt;/a&gt;，它支持把 kvcache 通过 redis 等方式进行共享。类比传统的应用相当于分布式 session（会话）管理。用户相关的 kvcache 在多个实例间共享，这样就不需要复杂的负载均衡策略。&lt;/p&gt;

&lt;p&gt;感谢 Yuandong Xie, Junyu Chen, Jingjing Zhou, Keming Yang 和 Zilong Cui 对本文的贡献。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 31 Dec 2024 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/openmodelz-journey-cn</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/openmodelz-journey-cn</guid>
      </item>
    
      <item>
        <title>2024 年终总结：Agent、AI Infra 与 Coding</title>
        <description>&lt;p&gt;今年是我的而立之年。我们的产品今年开始有了第一个付费客户，深深感到创业不易，身边的同事包括我都很辛苦。&lt;/p&gt;

&lt;p&gt;今年我的文章明显变少了，博客上所有的文章都是为了工作写的，但其实并不是表达欲望减少了，而是世界变化太快了，真正的空闲时间变得稀缺，我也更倾向于花更多时间在锻炼和陪伴家人上面。我的爱人经历了一些身体上的问题，身边好友的家人也有一些不幸的事情。这让我第一次真切地感受到了生活的无常。8 月份因为 TFCC 中断了三个月的锻炼，现在恢复后每周坚持 5 练。虽然能做的重量越来越大，但是体重和身形的变化并不显著。但很明显身体状态和精力都比之前好很多，现在每天 40 分钟的健身时间是我每天的 happy hour，休息日那天反而浑身难受。&lt;/p&gt;

&lt;p&gt;其他的时间里，相比写作，我更需要时间（也可能是更有兴趣）去学习 LLM 的新变化。跟去年相比我觉得这个领域更加好玩了，让我有点重新找到大一时候学习 Hello world 的那种新鲜感。&lt;/p&gt;

&lt;h2 id=&quot;agent&quot;&gt;Agent&lt;/h2&gt;

&lt;p&gt;早在两年前的 2023 年，我就在文章 &lt;a href=&quot;https://gaocegege.com/Blog/genai/ai-hype&quot;&gt;AI 应用层的壁垒在哪里&lt;/a&gt;里分享过我对 Agent 的观点：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;从更长远的时间来看，Agent 才是 AI 更值得期待的未来。如果说高效利用数据迭代模型的能力是 AI 时代的 TCP/IP，Agent 就是互联网本身。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;当初的那个比喻有点抽象，不过这个观点我到现在还是认为成立的。关于到底什么是 Agent，我最近有了一个更容易理解的答案（比喻太抽象不如不比喻）。&lt;/p&gt;

&lt;p&gt;我是 &lt;a href=&quot;https://poe.com/&quot;&gt;poe.com&lt;/a&gt; 重度用户，我会用 poe 来访问 GPT 和 Claude 来进行对话，多是问它一些编程问题。那么在这样的场景下，poe 就像是我的老师，它教给我怎么样去写代码来解决我的问题，但是它不能帮我直接写代码。LLM 是我的老师，但多数情况下我并不想做它的学生，我只是想做它的 executor。&lt;/p&gt;

&lt;p&gt;对于我想学习的知识，很多时候确实是 Claude 老师教我的我要认真学习。不过很多时候我的问题我不想知道答案，我只想 Claude 老师能帮我解决。比如我提问如何写一个 URL shortener，来把一个长的 URL 转换成短的 URL。这个问题我并不想知道答案，我只想知道怎么样用代码来实现这个功能。&lt;/p&gt;

&lt;p&gt;但是目前对话应用不能帮我写代码，那么我需要理解 Claude 教我的知识，然后通过自己的手来实现。这个过程我就在执行 Claude 的指令，在 VSCode 里写代码，然后在终端里运行代码，根据报错再次提问，然后再次循环。在这个过程里，我是 LLM 的智能版 executor。不仅在写代码这件事情上是这样，其他场景下也是这样。&lt;/p&gt;

&lt;p&gt;比如我想请 GPT 老师帮忙设计一个从上海到北京的旅行计划。在这些场景下我需要的是一个帮我解决问题的 executor，不仅要告诉我怎么做，更重要的是帮我做。帮我设定计划，预定机票，预定酒店，甚至帮我在旅行中解决我想不到的问题。&lt;/p&gt;

&lt;p&gt;通过与环境的交互，不断地学习和优化自己的行为，帮助我解决一个具体的问题而不是做我的军师，这就是 Agent。它把 LLM 当作大脑，把各种互联网应用当作工具，就如同我在使用 GPT 和 Claude 时的做法一样。所以 Agent 相当于是一个个硅基版的“人类”，提高了自动化水平。AI 的发展方向一直以来都是提高自动化水平，用 AI 替代人工和需要人的地方。那么 Agent 本身自然而然就是未来的方向。&lt;/p&gt;

&lt;h2 id=&quot;coding&quot;&gt;Coding&lt;/h2&gt;

&lt;p&gt;既然如此，我们来看看目前 Agent 进展最快的 coding 领域都发生了什么。GitHub Copilot 应该没人不知道吧，GitHub Copilot 最早只支持补全，这样的交互方式朴素一点来理解，与直接用 GPT 提问，然后把回复的代码复制粘贴到编辑器里没有太大区别（原理上类似，但肯定 Copilot 做了非常多工程工作）。这种方式没有办法进行多轮对话，通过迭代的方式来修改生成的结果。后来就有了单个文件内的 Ctrl + I 的 Modify（修改）功能。&lt;/p&gt;

&lt;p&gt;用户可以选定一段代码，然后按 Ctrl + I，Copilot 会通过一个对话框来询问用户想要做什么修改，如果不满意可以通过多次询问来修改代码。这一交互方式的改进，就像是 GPT 从最初 Jasper AI 的补全时代，进入了 ChatGPT 的对话时代。&lt;/p&gt;

&lt;p&gt;再后来，随着 Cursor、Windsurf 等工具的出现，多文件修改也被提出。虽然它很好用，但它仍然需要人的参与，甚至我自己觉得很多情况下，review 多文件修改内容所花费的时间比我用单文件修改一点点生成要更多一些。我需要 review 生成的代码，如果不是一次生成一个 commit 的话，如何 diff 这些变动都是一个问题。&lt;/p&gt;

&lt;p&gt;从单文件修改到多文件修改，就像是自动驾驶的 L2 到 L3，那 Agent 是 L4 级别的自动驾驶。Coding 场景下的一个例子就是 &lt;a href=&quot;https://v0.dev/&quot;&gt;v0.dev&lt;/a&gt;。Agent 应该能够在我极其少量的干预下，帮助我解决复杂的需求，它来帮助我调整和 review，生成高质量的代码。&lt;/p&gt;

&lt;p&gt;但就像自动驾驶领域一样，全自动无人驾驶谁都知道是未来的方向，但是什么时候能实现还是未知数。而且一出手就从 Agent 开始的产品（Devin 等），是不是合理的路径，我也不知道。在交互方式上，把最原始的补全的智能化水平提高，我反而觉得帮助最大。现在的补全很多时候是没什么逻辑的。举例说明，比如我写一篇文章，我在前文里写了我和家人一起去了北京和上海，在后续的段落里再次提到这件事，它会给我补全再加上很多没有被提到的城市。在代码补全里也存在这样的问题，补全在保持低延迟的同时正确识别我的意图，知道我想写的是什么，更加智能地补全，那其他形态也没有存在的必要了。&lt;/p&gt;

&lt;p&gt;Agent 和 Coding 今年成为了共识，但我觉得这俩方向属于天生的政治正确。直觉判断 Agent 这样的形态肯定是未来了，Coding 也是最适合用来探索提升自动化水平的场景。但是真的离我们很近么。就像我 2023 年时候的判断，Agent 是未来的方向，一年过去了，我没看出有离它越来越近。&lt;/p&gt;

&lt;p&gt;我自己比较看好的一些方向，包括为 Agent 提供更多获取数据的方式、和为 Agent 提供合适的运行环境。前者的产品有 &lt;a href=&quot;https://steel.dev/&quot;&gt;steel.dev/&lt;/a&gt; 等，还都非常初级。&lt;/p&gt;

&lt;h2 id=&quot;ai-infra&quot;&gt;AI Infra&lt;/h2&gt;

&lt;p&gt;接下来想分享一下我最最感兴趣的方向。为什么放到后面来讲，还是因为虽然喜欢但是小众。我看到的很多很好的工作我感觉都没有得到应有的关注度，比如 &lt;a href=&quot;https://github.com/flashinfer-ai/flashinfer&quot;&gt;FlashInfer&lt;/a&gt;。Casade Inference 在工业界感觉价值很大，但是一看&lt;a href=&quot;https://zhuanlan.zhihu.com/p/681514497&quot;&gt;知乎文章&lt;/a&gt; 80 多个赞，&lt;a href=&quot;https://news.ycombinator.com/from?site=flashinfer.ai&quot;&gt;Hacker News&lt;/a&gt; 上 2 个 points。&lt;/p&gt;

&lt;p&gt;回归正文，2022 年我们刚刚创业时候认为，AI infra 的易用性非常关键。PyTorch 就是很好的佐证，早在 2018 年刚开始学习 ML 框架的时候，Google 搜索 torch.distributed 的文章，简洁易懂。搜索一下 Tensorflow 分布式，乍一看我以为是玩抽象的。最后也证明 PyTorch 后来居上。&lt;/p&gt;

&lt;p&gt;在 22 年的时候我们认为 AI infra 应该以人为本，最简单的原因是因为人贵。那时候训练和推理的规模都不大，在推理的时候因为一块卡算力太强了，甚至还有很强的 GPU 虚拟化需求。那时候算法工程师的模型能力更加稀缺，所以我们认为 AI infra 应该以算法工程师为核心，于是我们做了 &lt;a href=&quot;https://github.com/tensorchord/envd&quot;&gt;envd&lt;/a&gt; 简化开发环境的构建过程。&lt;/p&gt;

&lt;p&gt;站在 2024 年的末尾来看，我认为易用性已经不再是绝大多数场景下的 AI infra 的核心问题。原因也很简单，以前以人为本，现在以机器为本。看看英伟达的股价，应该能够理解。现在不管是预训练、还是推理，都不是小团队需要考虑的事情了。大模型公司是新的创业公司，但是是 AI 领域极其专业的大团队了。以前之所以易用性重要，部分原因是 GPU 确实不好用。CUDA 加上容器虚拟化，会让镜像的分发，启动，调度等等都变得非常复杂，更不用说配上经常出问题的 IB。&lt;/p&gt;

&lt;p&gt;现在这些事情，大部分 AI 公司不需要考虑那么多。需要考虑这些的公司也都很专业，他们有专业的团队来解决这些问题。所以现在的 AI infra 更多的诉求是提高机器的利用率，这就是 FlashInfer, PagedAttention, RadixAttention（RadixAttention 跟 Attention 的关系到底在哪里🤔）, LLMCache 等等工作的重点。&lt;/p&gt;

&lt;p&gt;最近这个领域的工作，有点梦回 Hadoop 刚出来的时候。学术界动不动就有论文把 Hadoop 当作 Baseline 提高个几十几百倍的性能。系统领域有了新的场景还能产生真实的工业界价值，这是我最喜欢的地方。&lt;/p&gt;

&lt;p&gt;与系统关系越来越深的训练与推理领域，我认为易用性变得越来越不重要。但是在 Low Code 和 No Code 领域，易用性依然是最重要的。我现在对于训练和推理场景的 AI infra 从业者的刻板印象是华人/国人系统模型双修猛男程序员。为他们做 infra 就是 flashinfer 的情况，同行拍手称快，其他人看不明白。&lt;/p&gt;

&lt;p&gt;广义上的 AI infra 更加广大的用户群体是不会 AI 的普通人。怎么让我爸利用 AI 在工作中需要把一个网页内的数据做成 excel，这样的 Low Code 和 No Code 的场景，易用性依然是最重要的。2023 年我第一次看 dify 会觉得它的 UIUX 做的很好，但我不了解我什么场景下需要它。现在看我不是它的受众，但是&lt;strong&gt;正因为我不是它的受众它才有光明的未来&lt;/strong&gt;。所以说 Luyu 是一个很好的产品经理，他能够看到我 2024 年才看得到的需求。&lt;/p&gt;

&lt;p&gt;今年我最感兴趣的 AI infra 产品是 &lt;a href=&quot;https://github.com/skypilot-org/skypilot&quot;&gt;Skypilot&lt;/a&gt;。我一直认为 Kubernetes 对于 AI 负载来说是一个及格的产品但不是一个好用的产品。但是 build a scheduling system for AI from scratch 又有非常大的替换成本。Skypilot 相当于又在 Kubernetes 之上抽象了一层，为用户提供了更好的 UIUX。&lt;/p&gt;

&lt;p&gt;Kubernetes 凭借着它的扩展性，让它一步步成为了公有云 PaaS 服务的标准界面。只要你会用 kubectl，你就可以在任何云上部署你的服务。而 Skypilot 为 AI 场景提供了更合理的界面（Terminal UI &amp;amp; CLI），未尝不能成为 PaaS 之上 AI PaaS 的标准界面。之前不存在这样的可能大部分原因是因为&lt;strong&gt;过去的 AI 实在太非标&lt;/strong&gt;了，不同场景的模型千差万别，所以那个时候需要的是 Kubeflow、KServe。&lt;/p&gt;

&lt;p&gt;倘若未来 LLM 全都是 Decoder only 的 Transformer，它的 pattern 非常统一，那么如果针对这一 pattern 做好抽象，支持路由层面的 prefix cache aware 的 load balance 等等最佳实践，是不是有可能呢。当然任何技术都需要 tradeoff，Skypilot 带来新抽象的同时也带来了新的复杂度。但是我是很久没有看到让我感兴趣的调度领域的工作了，Kubernetes 杀死了比赛，现在随着模型的统一化，又有了新的机会。&lt;/p&gt;

&lt;p&gt;最后，祝大家新年快乐，如果你有什么想法或者问题，欢迎留言。&lt;/p&gt;

&lt;h2 id=&quot;往年总结&quot;&gt;往年总结&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2023&quot;&gt;2023&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2022&quot;&gt;2022&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2021&quot;&gt;2021&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2020&quot;&gt;2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2019&quot;&gt;2019&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2018&quot;&gt;2018&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2017&quot;&gt;2017&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2016&quot;&gt;2016&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2015&quot;&gt;2015&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/record&quot;&gt;2014&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 31 Dec 2024 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2024</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/%E9%9A%8F%E7%AC%94/newyear2024</guid>
      </item>
    
      <item>
        <title>为什么 HNSW 不是最终的答案</title>
        <description>&lt;p&gt;2024.12.24: 本文部分评论可见 &lt;a href=&quot;https://news.ycombinator.com/item?id=42496465&quot;&gt;Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Original post: &lt;a href=&quot;https://blog.pgvecto.rs/why-hnsw-is-not-the-answer&quot;&gt;Why HNSW is Not the Answer&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;HNSW 已经成为许多向量数据库的热门算法。它的多层次图结构和高效处理向量嵌入的能力确实很吸引人。不过，尽管 HNSW 看起来优势明显，但在大规模向量相似性搜索中，它可能不是最佳选择。在这篇博文中，我们将探讨 HNSW 的主导地位，并讨论为什么像 IVF（倒排文件索引）这样的基于磁盘的替代方案，可能对于处理大规模数据集更为实用。&lt;/p&gt;

&lt;h2 id=&quot;hnsw-的优势&quot;&gt;HNSW 的优势&lt;/h2&gt;

&lt;p&gt;HNSW 具有几个显著优势：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;高效搜索：其基于图的结构能够快速进行最近邻搜索，尤其适用于较小的数据集。&lt;/li&gt;
  &lt;li&gt;增量更新：可以增量地添加新向量，而无需重建索引，这对于动态环境来说是一个重要优势。&lt;/li&gt;
  &lt;li&gt;高召回率：HNSW 提供了高召回率和相对较低的延迟，非常适合实时应用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不过，随着数据集规模的增长，HNSW 的一些缺点也变得更加明显。&lt;/p&gt;

&lt;h2 id=&quot;hnsw-的问题&quot;&gt;HNSW 的问题&lt;/h2&gt;

&lt;p&gt;HNSW 面临的主要挑战之一是对内存的高度依赖，尤其是在索引和搜索操作中。以下是主要问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;内存开销：遍历 HNSW 的图结构涉及高度随机的访问模式。为了获得合理的性能，整个数据集必须存储在内存中。这对于拥有数十亿高维向量的大规模数据集来说，内存需求过于庞大，变得不可行。&lt;/li&gt;
  &lt;li&gt;对内存大小的性能敏感性：如果内存稍微不足以容纳所有向量，HNSW 的性能会急剧下降。在这种情况下，数据交换到磁盘会严重影响搜索速度，使得在内存受限的系统中使用变得不切实际。&lt;/li&gt;
  &lt;li&gt;不适合基于磁盘的环境：HNSW 本质上是为内存操作设计的，在以磁盘为导向的场景（如 PostgreSQL）中表现不佳。其对频繁随机访问模式的依赖，使其与磁盘存储的顺序访问特性不兼容。&lt;/li&gt;
  &lt;li&gt;插入和删除成本：在 HNSW 中更新索引需要在整个图中进行级联修改，导致显著的计算和写放大。这使得插入和删除操作既缓慢又资源密集。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;相比之下，像 IVF 这样的基于磁盘的解决方案在需要扩展性和效率的场景中表现出色。&lt;/p&gt;

&lt;h2 id=&quot;为什么-ivf-可能比-hnsw-更快&quot;&gt;为什么 IVF 可能比 HNSW 更快&lt;/h2&gt;

&lt;p&gt;一项关键观察（Key insight）是，所有向量搜索算法的性能在很大程度上取决于它们执行的距离计算次数。减少这些计算对于提高搜索速度至关重要。虽然原始的 IVF 索引需要扫描总向量的 1% 到 5%，但现代在量化和优化方面的进展显著提升了其效率，使 IVF 成为 HNSW 的强有力竞争者。&lt;/p&gt;

&lt;p&gt;以下是 IVF 可能更快的几个原因：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;减少距离计算：IVF 通过将数据集划分为多个簇，显著减少了需要计算的向量数量。搜索时只需在相关簇内进行距离计算，从而提高了搜索速度。&lt;/li&gt;
  &lt;li&gt;优化的量化技术：现代量化方法能够有效地压缩数据，同时保留重要信息，这进一步加快了搜索过程。&lt;/li&gt;
  &lt;li&gt;较低的内存需求：IVF 设计为更适合磁盘操作，减少了对内存的依赖，使其在处理大规模数据集时更加高效。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;随着技术的发展，IVF 在速度和效率上已经成为一个值得关注的选择，尤其是在处理大规模向量时。&lt;/p&gt;

&lt;h2 id=&quot;quantization改变游戏规则的关键&quot;&gt;Quantization：改变游戏规则的关键&lt;/h2&gt;

&lt;p&gt;量化通过将高维向量压缩为紧凑的表示，显著提高了向量搜索的效率。以下是一些重要的量化方法：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;RaBitQ：这一发表在 SIGMOD’24 上的工作利用测量集中现象提升二进制和标量量化的准确性，将每个维度量化为 1 位，实现 32 倍的压缩比，并且相比于 PQ 能够保证明确的 error bound。&lt;/li&gt;
  &lt;li&gt;产品量化（PQ）：通过将向量空间划分为多个子空间，独立量化每个子空间，以最小化近似误差。此方法在 FAISS 中提供从 4 倍到 64 倍的灵活压缩比，实现更精细的压缩和更快的近似搜索。&lt;/li&gt;
  &lt;li&gt;标量量化（SQ）：通过将每个向量维度的范围划分为离散级别，独立量化每个维度。通常通过从浮点数转换为 int8，实现约 4 倍的压缩比。&lt;/li&gt;
  &lt;li&gt;ScaNN：采用各向异性向量量化来优化内积准确性，通过惩罚影响高内积的方向错误，达到更优的召回率和速度。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;量化显著减少了内存和磁盘空间的使用——将浮点数转换为比特时，减少的比例通常可达 32 倍，同时大幅降低计算开销。尽管会有一些精度损失，但量化使向量比较变得更加高效。例如，两个 D 维向量之间的典型距离计算复杂度为 O(D²)，但将浮点数压缩为比特后，这一复杂度降低了 1024 倍（32x32）。通过快速扫描优化，这些计算通过高效的 CPU 寄存器查找进一步加速。结合 IVF，许多量化方法在速度和可扩展性方面始终优于 HNSW。&lt;/p&gt;

&lt;p&gt;典型工作流程非常简单：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;初始搜索：利用压缩表示快速识别候选向量。&lt;/li&gt;
  &lt;li&gt;重新排序（rerank）：在较小的候选子集上使用全精度距离计算来细化结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过这些方法，量化不仅提升了搜索速度，也为处理大规模数据集提供了更好的解决方案。&lt;/p&gt;

&lt;h2 id=&quot;比较-rabitqivf-和-hnsw&quot;&gt;比较 RaBitQ+IVF 和 HNSW&lt;/h2&gt;

&lt;p&gt;RabitQ 是新近提出的一种量化方法，它将 32 位向量压缩为 1 位。虽然这种压缩会导致一些精度损失，但大大降低了计算要求。通过快速扫描优化，我们可以实现比传统向量距离计算快 100 倍以上的计算。&lt;/p&gt;

&lt;p&gt;结合量化的 IVF 提供了一种高效的方式来存储所有量化向量。通过利用 RaBitQ，内存使用量相比于全尺寸向量减少了 32 倍，允许整个量化数据集适配于内存。这种设计使得索引能够快速检索近似最近邻，并使用从磁盘获取的全精度向量进行重新排序。对于典型的 Top-10 查询，IVF 只需从磁盘获取 100-200 个向量，而 HNSW 可能需要 800-1000 个向量。这种高效的方法在内存使用和磁盘访问之间达成了最佳平衡，提供了卓越的性价比。&lt;/p&gt;

&lt;p&gt;尽管理论上可以将类似的量化技术应用于 HNSW，但实际约束降低了其有效性：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Vector Packing：Fast Scan 优化依赖于将 32 个压缩向量打包在一起，而这与 HNSW 的图结构不兼容。&lt;/li&gt;
  &lt;li&gt;随机访问成本：HNSW 涉及在图节点和边之间频繁的随机访问，使得遍历效率低下。相比之下，IVF 将向量顺序组织在发布列表中，便于更快的预取和有效的顺序扫描。&lt;/li&gt;
  &lt;li&gt;重新排序成本：IVF 和 HNSW 在重新排序计算成本上相似，因为它们在第一阶段都依赖于量化表示。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;尽管量化 IVF 和 HNSW 之间的性能差异可能很小，但 IVF 以其简洁性和高效性脱颖而出。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;IVF&lt;/th&gt;
      &lt;th&gt;IVF + RaBitQ&lt;/th&gt;
      &lt;th&gt;HNSW&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Indexing Method&lt;/td&gt;
      &lt;td&gt;KMeans can be offloaded to GPU&lt;/td&gt;
      &lt;td&gt;KMeans + Quantization&lt;/td&gt;
      &lt;td&gt;Nearest Neighbor Graph, need to keep everything in memory&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Overlapped Data Across Query&lt;/td&gt;
      &lt;td&gt;Centroids&lt;/td&gt;
      &lt;td&gt;Quantized vectors&lt;/td&gt;
      &lt;td&gt;No&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Scalability&lt;/td&gt;
      &lt;td&gt;Limited by CPU and memory&lt;/td&gt;
      &lt;td&gt;Outstanding&lt;/td&gt;
      &lt;td&gt;Limited by memory&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Insertion/Deletion&lt;/td&gt;
      &lt;td&gt;Simple (updates posting lists)&lt;/td&gt;
      &lt;td&gt;Simple (updates posting lists)&lt;/td&gt;
      &lt;td&gt;Complex (cascading graph changes)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Search Speed&lt;/td&gt;
      &lt;td&gt;Slow&lt;/td&gt;
      &lt;td&gt;Extremely Fast with Quantization&lt;/td&gt;
      &lt;td&gt;Fast with sufficient memory&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Overall Complexity&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;ivf-的操作简便性&quot;&gt;IVF 的操作简便性&lt;/h2&gt;

&lt;p&gt;IVF 的简洁性使其成为现实世界应用中更实用的选择：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;插入和删除：在 HNSW 中，这些操作会触发图中的级联修改，导致显著的计算和写放大。相比之下，IVF 只需更新相关的发布列表。&lt;/li&gt;
  &lt;li&gt;基于磁盘的存储：IVF 依赖于基于磁盘的索引，使其能够高效地扩展，而不需要 HNSW 所需的高昂内存成本。&lt;/li&gt;
  &lt;li&gt;适应性强：IVF 可以轻松与先进的量化技术结合，进一步优化性能。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;

&lt;p&gt;尽管 HNSW 已经巩固了其作为快速准确的向量相似性搜索算法的声誉，但它并非没有局限性。其内存密集型的特性和操作复杂性使其不太适合大规模应用。相反，IVF 提供了一种可扩展且具有成本效益的替代方案，特别是当与现代量化技术结合时。&lt;/p&gt;

&lt;p&gt;随着对向量搜索需求的持续增长，实践者必须仔细评估内存基础和基于磁盘的解决方案之间的权衡。HNSW 可能在小到中型应用中占主导地位，但对于大规模数据集，应该超越 HNSW，采用像 IVF 这样更简单、更可扩展的方法。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 25 Dec 2024 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/hnsw</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/hnsw</guid>
      </item>
    
      <item>
        <title>VectorChord：在 PostgreSQL 中以 1 美元的价格存储 40 万 Vectors</title>
        <description>&lt;p&gt;2024.12.24: 本文部分评论可见 &lt;a href=&quot;https://news.ycombinator.com/item?id=42324059&quot;&gt;Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Original post: &lt;a href=&quot;https://blog.pgvecto.rs/vectorchord-store-400k-vectors-for-1-in-postgresql&quot;&gt;VectorChord: Store 400k Vectors for $1 in PostgreSQL&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我们很高兴地宣布推出适用于 PostgreSQL 的新向量搜索扩展，它提供了一种非常经济高效的方法来管理大型向量。使用 &lt;a href=&quot;https://github.com/tensorchord/VectorChord&quot;&gt;VectorChord&lt;/a&gt;，您可以对 top 10 查询的 1 亿个 768 维向量实现 131 的 QPS 和 0.95 的精度。此设置每月仅需 250 美元，并且可以托管在一台机器上。&lt;/p&gt;

&lt;p&gt;这意味着只需 1 美元即可存储 400k 个向量，从而大幅节省成本：与 Pinecone（存储优化实例）相比，向量数量多 6 倍，与 pgvector/pgvecto.rs 相比，价格相同，向量数量多 26 倍。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/1.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Vectors for $1&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;在基于 MyScale Benchmark 数据的向量存储月度成本比较中，突出展示了 VectorChord 如何成为一种经济实惠的选择，存储 1 亿个向量的价格仅为 247 美元。相比之下，尽管 Pinecone 的存储经过了优化，但每月成本为 1,600 美元，而 Qdrant 的价格为 4,374 美元。pgvector/pgvecto.rs 的成本要高得多，为 6,580 美元。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/2.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Monthly cost&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;hnsw-的问题&quot;&gt;HNSW 的问题&lt;/h2&gt;

&lt;p&gt;作为 PGVecto.rs 的继任者，VectorChord 从其前身中获得了宝贵的见解。虽然许多矢量数据库或扩展（包括 PGVecto.rs）在处理约 100 万个数据集时表现良好，但它们在扩展到更大的规模时往往会遇到困难，例如从 1000 万到 1 亿。传统的基于 HNSW 的矢量数据库在处理更大的数据集时面临特定挑战：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;索引构建时间长：通常需要 2 个多小时才能为 500 万条记录构建索引。&lt;/li&gt;
  &lt;li&gt;高内存要求：存储 1000 万个向量的数据集可能需要多达 40GB 的内存。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;vectorchord-的解决方案磁盘友好型-ivfrabitq&quot;&gt;VectorChord 的解决方案：磁盘友好型 IVF+RabitQ&lt;/h2&gt;

&lt;p&gt;VectorChord 采用 IVF（倒排文件索引）和 RaBitQ[1] 量化来提供快速、可扩展且准确的向量搜索功能。此方法将 32 位向量压缩为紧凑的位表示，从而显著减少计算需求。大多数比较都是使用这些压缩向量进行的，而全精度计算则保留用于应用于较小子集的自适应重新排序阶段，以确保速度和召回率均得到保留。&lt;/p&gt;

&lt;p&gt;许多人认为 IVF 的召回率/速度权衡不如 HNSW，并且需要进行许多优化配置。然而，这是一个复杂的问题，我们现在将简要解释一下，稍后会发布有关该主题的详细文章。&lt;/p&gt;

&lt;h3 id=&quot;ivf-vs-hnsw&quot;&gt;IVF vs. HNSW&lt;/h3&gt;

&lt;p&gt;向量搜索算法花费的时间中很大一部分用于距离计算。为了提高速度，必须尽可能减少距离比较。原始 IVF 在这方面遇到了困难，通常需要扫描总向量的 1% 到 5%，这比 HNSW 的要求高得多。&lt;/p&gt;

&lt;p&gt;然而，RabitQ 提出了一种创新方法，可以将 32 位向量压缩为 1 位。虽然这种压缩会导致一些精度损失，但大大降低了计算要求。通过快速扫描优化，我们可以实现比传统向量距离计算快 100 倍以上的计算。&lt;/p&gt;

&lt;p&gt;您可能想知道召回率。我们可以重新排序其他向量以提高召回率，并且只有在重新排序阶段才需要进行全精度距离计算。RaBitQ 保证了严格的理论误差界限，同时提供了良好的经验准确性。这就是 IVF 比 HNSW 更快的原因。&lt;/p&gt;

&lt;p&gt;以下是 GIST 数据集的一些初始基准测试结果，该数据集包含 960 个维度的 100 万个向量。在召回率相同的情况下，VectorChord 的 QPS 可能是 pgvector 的两倍。更多详细信息将在 Benchmark 章节提供。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/3.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;GIST 1M&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;external-index-build&quot;&gt;External Index build&lt;/h3&gt;

&lt;p&gt;原始 IVF 方法通常需要扫描 1-5% 的数据集，这可能会很慢。通过使用 RaBitQ 和快速扫描优化，VectorChord 旨在通过减少需要完全比较的全精度向量数量来加快计算速度。这种方法有助于创建一个稳定且可扩展的向量搜索系统，该系统可与 PostgreSQL 存储系统很好地配合使用。因此，用户可以将物理复制和其他 PostgreSQL 功能与 VectorChord 一起使用。&lt;/p&gt;

&lt;p&gt;VectorChord 基于 IVF 构建，允许在外部（例如在 GPU 上）进行 KMeans 聚类并轻松导入数据库。我们在具有 2 个 vCPU 和 16 GB RAM 的 AWS i4i.large 实例上执行了测试以测量索引和插入时间。用于此测试的数据集是 GIST 1M。我们插入了 700,000 个向量，构建了索引，然后添加了另外 300,000 个向量。在预热系统后，我们使用单个线程执行查询。在这个过程中，我们评估了索引的构建时间和插入时间。结果如下：&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/4.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;GIST 1M&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;VectorChord 使用一台单独的机器进行 KMeans 聚类，构建索引耗时 186 秒，比 pgvector 快 16 倍。此外，插入时间也比 pgvector 快 14 倍。众所周知，索引是向量数据库中最耗资源的部分，需要大量计算，增加了对 CPU 和内存的需求。通过使用更强大的机器构建索引，然后将其导入到更小的机器进行查询，可以在单台机器上支持数十亿个向量。&lt;/p&gt;

&lt;h2 id=&quot;benchmark&quot;&gt;Benchmark&lt;/h2&gt;

&lt;p&gt;我们进行了额外的实验，使用 LAION 5M 和 100M 数据集更彻底地评估性能和成本。&lt;/p&gt;

&lt;h3 id=&quot;laion-5m&quot;&gt;LAION 5M&lt;/h3&gt;

&lt;p&gt;我们使用 LAION 5M 数据集进行了实验，结果对 Vectorchord 来说令人鼓舞。与其他平台相比，它始终实现更高的每秒查询数 (RPS)。虽然许多数据库在召回率提高时难以在速度和准确率之间保持平衡，但 Vectorchord 即使在更高的召回率水平下也能保持高效。这一特性使其成为需要快速响应和准确率的应用程序的合适选择。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/5.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;LAION 5M Top 10&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;实验采用 Myscale Benchmark，在一台 r6a.xlarge 机器上进行，该机器具有 4 个 vCPU、32GB 内存和 200GB EBS 存储。实验设置的参数包括 nlist 为 8192、共享缓冲区为 28GB、JIT 禁用、有效 I/O 并发数为 200。我们在没有预热的情况下进行了两次实验。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/6.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;LAION 5M Top 100&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;我们使用的机器是一台 r6a.xlarge，每月成本约为 165.56 美元，我们在 LAION 5M 数据集的 Top 100 中取得了相当的性能。&lt;/p&gt;

&lt;h3 id=&quot;单机支持-laion-100m&quot;&gt;单机支持 LAION 100M&lt;/h3&gt;

&lt;p&gt;此外，由于其磁盘友好的索引功能，增加单台机器的磁盘容量可以成比例地提高 VectorChord 可以容纳的最大向量数量，可能允许存储 10 亿或更多。&lt;/p&gt;

&lt;p&gt;为了评估可扩展性，我们使用 AWS i4i.xlarge 实例对 LAION 100M 数据集（768 个维度）进行了实验，这是一个经济实惠的配置，每月价格为 250 美元。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/7.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;它只有 4 个 CPU 和 32 GB 内存，其中 937 GB 的 SSD 用于存储 1 亿个向量。在此设置下，我们通过单线程查询实现了前 10 个结果的 QPS 为 16.2 @ recall 0.95，前 100 个结果的 QPS 为 4.3 @ recall 0.95。以下是令人印象深刻的结果：&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/vectorchord/8.png&quot; alt=&quot;vectorchord&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;在保持召回率大于0.95的前提下，我们还在这台4vCPU的机器上测试了多线程QPS，在这个场景下，随着请求线程数从1个增加到8个，向量查询的QPS可以线性提升，说明VectorChord具有很好的可扩展性。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;VectorChord 是专为高效向量搜索而设计的一款新 PostgreSQL 扩展。它允许用户仅用 1 美元存储 400,000 个向量，比竞争对手便宜得多。通过利用 IVF 和 RaBitQ 量化，VectorChord 优化了搜索速度和内存使用率，使其适用于大型数据集。&lt;/p&gt;

&lt;p&gt;我们在 PGVecto.rs Cloud 为 VectorChord 提供云托管服务。我们的平台简化了部署和管理，使您能够轻松高效地扩展向量数据库解决方案。如果您对 VectorChord 有任何疑问，请随时联系我们。我们随时为您提供帮助！您可以在我们的存储库中打开问题，也可以发送电子邮件至 vectorchord-inquiry@tensorchord.ai。&lt;/p&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 18 Dec 2024 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/vectorchord</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/vectorchord</guid>
      </item>
    
      <item>
        <title>My binary vector search is better than your FP32 vectors</title>
        <description>&lt;p&gt;2024.12.24: 本文部分评论可见 &lt;a href=&quot;https://news.ycombinator.com/item?id=39823512&quot;&gt;Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Original post: &lt;a href=&quot;https://blog.pgvecto.rs/my-binary-vector-search-is-better-than-your-fp32-vectors&quot;&gt;My binary vector search is better than your FP32 vectors&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在向量搜索领域出现了一项有趣的发展：二进制向量搜索。这种方法通过显著减少内存消耗，取得了30倍的减少。然而，关于它对准确性的影响引发了争议。然而通过实验我们发现，使用二进制向量搜索和特定的优化技术，可以保持与原始向量相似的准确性。为了阐明这个问题，我们展示了一系列实验来演示这种方法的效果和影响。&lt;/p&gt;

&lt;h2 id=&quot;二进制向量搜索&quot;&gt;二进制向量搜索&lt;/h2&gt;

&lt;p&gt;二进制向量是一种向量的表示方式，其中向量中的每个元素被编码为二进制值，通常为0或1。这种编码方案将原始向量（可能包含实值或高维数据）转换为二进制格式。&lt;/p&gt;

&lt;p&gt;二进制向量只需要占用一个 bit 来存储每个元素，而原始的 float32 向量需要每个元素占用 4 个字节。这意味着使用二进制向量可以将内存使用量减少多达 32 倍。此外，内存需求的减少还可以显著增加二进制向量操作的每秒请求数（RPS）。&lt;/p&gt;

&lt;p&gt;举个例子，考虑这样一个情况：我们有 100 万个向量，每个向量在 3072 维空间中由 float32 值表示。如果我们使用原始的 float32 向量，存储所有向量需要约 20GB 的内存。而如果我们使用二进制向量，仅需约 600MB 的内存即可存储所有 100 万个向量。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/memusage.png&quot; alt=&quot;pgvectors&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Memory Usage (1M vectors)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;按照直觉来说，二值化会导致准确性显著降低，因为二进制向量丢失了许多原始信息。令人惊讶的是，我们的实验结果显示，准确性的降低并不像预期的那样大。尽管二进制向量失去了一些具体细节，但它们仍然能够捕捉到重要的模式和相似性，从而保持了相当的准确性。&lt;/p&gt;

&lt;h2 id=&quot;实验&quot;&gt;实验&lt;/h2&gt;

&lt;p&gt;为了评估与原始向量方法相比的性能指标，我们使用了 &lt;a href=&quot;https://huggingface.co/datasets/Qdrant/dbpedia-entities-openai3-text-embedding-3-large-3072-1M&quot;&gt;dbpedia-entities-openai3-text-embedding-3-large-3072-1M&lt;/a&gt; 数据集进行基准测试。该基准测试是在Google Cloud虚拟机（VM）上进行的，该虚拟机规格为 n2-standard-8，包括 8 个虚拟 CPU 和 32GB 内存。我们使用了 &lt;a href=&quot;https://github.com/tensorchord/pgvecto.rs&quot;&gt;pgvecto.rs v0.2.1&lt;/a&gt; 作为向量数据库。&lt;/p&gt;

&lt;p&gt;在将一百万个向量插入数据库表后，我们为原始的 float32 向量和二进制向量都建立了索引。&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai3072&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bigserial&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;KEY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_embedding&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3072&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_bvector&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3072&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INDEX&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai_vector_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai3072&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vectors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_embedding&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vector_l2_ops&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INDEX&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai_vector_index_bvector&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;openai3072&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;USING&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vectors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_bvector&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bvector_l2_ops&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在建立索引之后，我们进行了向量搜索查询以评估性能。这些查询使用不同的限制进行执行，表示要检索的搜索结果数量（限制为 5、10、50、100）。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/binary-bench.avif&quot; alt=&quot;pgvectors&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;二进制向量 Benchmark&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;我们观察到，二进制向量搜索的每秒请求数（RPS）约为 3000，而原始向量搜索的 RPS 仅约为 300。RPS 指标表示系统每秒能够处理的请求或查询数量。较高的 RPS 值意味着更高的吞吐量和更快的响应时间。然而，与原始向量搜索相比，二进制向量搜索的准确性降低到约 80%。在某些情况下，这可能被视为不可接受，特别是在需要高准确性的关键情况下。&lt;/p&gt;

&lt;h2 id=&quot;优化adaptive-retrieval&quot;&gt;优化：Adaptive Retrieval&lt;/h2&gt;

&lt;p&gt;幸运的是，我们有一种简单而有效的方法，称为自适应检索（adaptive retrieval），我们从 &lt;a href=&quot;https://aniketrege.github.io/blog/2024/mrl/#what-is-mrl-really-this-time&quot;&gt;Matryoshka Representation Learning&lt;/a&gt; 中学到了这个方法，可以提高准确性。&lt;/p&gt;

&lt;p&gt;虽然名字听起来很复杂，但自适应检索的思想很简单。假设我们想找到最佳的 100 个候选项，我们可以按照以下步骤进行操作：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;通过查询二进制向量索引从 1 百万个嵌入中检索出一个较大的集合（例如 200 个候选项）。这是一个快速的操作。&lt;/li&gt;
  &lt;li&gt;使用 KNN 查询对候选项重新排序，以获取排名前 100 的候选项。请注意，我们使用 KNN 而不是 ANN 进行重新排序。在需要处理较小集合并进行准确相似性搜索的场景中，KNN 很适用，因此在这种情况下对候选项进行重新排序是一个很好的选择。&lt;/li&gt;
&lt;/ol&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/ar.avif&quot; alt=&quot;adaptive-retrieval&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Adaptive Retrieval&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;结合二进制向量搜索的效率和 KNN 重新排序的准确性，我们可以在检索过程中既实现速度又提高准确性。通过引入这个重新排序步骤，我们可以显著提高准确性，潜在地达到高达 95% 的准确率。此外，系统仍然保持着高的每秒请求数（RPS），大约为 1700。此外，尽管这些改进，索引的内存使用仍然显著较小，约为原始向量表示的 30 倍。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/ar-bench.png&quot; alt=&quot;bench&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Adaptive Retrieval Benchmark&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;以下是我们在 PostgreSQL 中实现 Adaptive Retrieval 的 SQL 函数：&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;REPLACE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FUNCTION&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match_documents_adaptive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;query_embedding&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3072&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;match_count&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RETURNS&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SETOF&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai3072&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;LANGUAGE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SQL&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- Step 1: Query binary vector index to retrieve match_count * 2 candidates&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WITH&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shortlist&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openai3072&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_bvector&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;binarize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query_embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- Step 2: Rerank the candidates using a KNN query to retrieve the top candidates&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shortlist&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text_embedding_3_large_3072_embedding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query_embedding&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;与-shortened-vectors-的比较&quot;&gt;与 Shortened vectors 的比较&lt;/h2&gt;

&lt;p&gt;OpenAI 最新的 embedding 模型 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-embedding-3-large&lt;/code&gt; 具有一项功能，允许用户截断向量直接使用。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/shortening-embedding.svg&quot; alt=&quot;bench&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Shortened Vector&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;该模型默认生成 3072 维的嵌入向量。但是可以安全地从序列的末尾删除一些数字，仍然能够保持文本的有效表示。例如，可以将嵌入向量缩短为 1024 维。这个功能可以节省内存并加快请求速度，就像二进制向量一样。&lt;/p&gt;

&lt;p&gt;然而根据我们的实验，结论很明确：二进制向量明显优于缩短向量。&lt;/p&gt;

&lt;p&gt;我们进行了类似的基准测试来与二进制向量进行比较。我们使用相同的数据集和机器类型创建了两个索引，但维度不同。一个索引有 256 维，另一个索引有 1024 维。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/first-pass.png&quot; alt=&quot;bench&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Shortened Vector Benchmark&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;1024 维索引在每秒 1000 个请求（RPS）的情况下实现了约 85% 的准确率。另一方面，256 维索引在每秒 1200 个请求（RPS）的情况下准确率约为 60%。&lt;/p&gt;

&lt;p&gt;1024 维索引需要约 8GB 的内存，而256 维索引则使用约 2GB 的内存。相比之下，二进制向量方法在每秒 3000 个请求（RPS）的情况下实现了约 80% 的准确率，其内存使用量约为 600MB。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/memusage.png&quot; alt=&quot;memory&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Memory Usage (1M vectors)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;我们使用较低维度的索引实现了自适应检索。在请求速率（RPS）和准确率方面，二进制向量索引仍然优于 256 维索引，并且内存使用量更低。另一方面，自适应检索配合 1024 维索引实现了更高的准确率（99%）；然而，它的请求速率相对较低，并且与其他索引相比，内存使用量增加了 12 倍。&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/final-bench.png&quot; alt=&quot;bench&quot; height=&quot;500&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Benchmark&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;

&lt;p&gt;通过利用自适应检索技术，二进制向量可以在显著减少 30 倍的内存使用量的同时保持高水平的准确性。我们在表格中展示了基准指标以展示结果。需要注意的是，这些结果是特定于 OpenAI text-embedding-3-large 模型的&lt;/p&gt;

&lt;figure&gt;
	&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;/Blog/images/binary-vector/table.png&quot; alt=&quot;bench&quot; height=&quot;700&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Benchmark&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;license&quot;&gt;License&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;This article is licensed under &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/3.0/&quot;&gt;CC BY-NC-SA 3.0&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Please contact me for commercial use.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 25 Mar 2024 00:00:00 +0800</pubDate>
        <link>https://gaocegege.com/Blog/genai/binary-vector-search</link>
        <guid isPermaLink="true">https://gaocegege.com/Blog/genai/binary-vector-search</guid>
      </item>
    
  </channel>
</rss>
