<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Python on Wilson Wu</title><link>https://wilsonwu.me/zh/tags/python/</link><description>Recent content in Python on Wilson Wu</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 13 Aug 2026 00:00:00 -0400</lastBuildDate><atom:link href="https://wilsonwu.me/zh/tags/python/index.xml" rel="self" type="application/rss+xml"/><item><title>OpenWebUI 数据库迁移实践：从 VM 自建 PostgreSQL 平滑迁移到 Azure 托管 PostgreSQL</title><link>https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/</link><pubDate>Thu, 13 Aug 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/</guid><description>&lt;p&gt;&lt;img alt="迁移" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/migrate-openwebui-to-azure-pg/1-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;随着 OpenWebUI 中的用户、对话、文件和知识库数据不断增长，原先部署在 Azure VM 上的自建 PostgreSQL 带来了越来越多的运维工作，包括数据库补丁、备份、故障恢复、存储监控和安全维护。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="迁移" loading="lazy" src="/zh/blog/2026/migrate-openwebui-to-azure-pg/1-overview.png"></p>
<p>随着 OpenWebUI 中的用户、对话、文件和知识库数据不断增长，原先部署在 Azure VM 上的自建 PostgreSQL 带来了越来越多的运维工作，包括数据库补丁、备份、故障恢复、存储监控和安全维护。</p>
<p>为了降低维护成本并提高数据库可靠性，我将 <strong>OpenWebUI 的业务数据库</strong>从 VM 本地 PostgreSQL 迁移到了 <strong>Azure Database for PostgreSQL Flexible Server</strong>。</p>
<p>这次迁移采用了：</p>

<blockquote>
  <p>在线全量迁移 + 数据一致性验证 + 活跃对话增量同步 + 快速切换</p>

</blockquote>
<p>全量数据库导出和恢复期间，OpenWebUI 始终保持在线。最终切换仅涉及一次很短的 Docker 容器重建，用户侧基本无感。</p>

<blockquote>
  <p>本文中的服务器地址、数据库名称、用户名、密码和资源标识均已隐藏或替换为占位符。</p>

</blockquote>
<h2 id="迁移背景">迁移背景</h2>
<p>OpenWebUI 以 Docker 容器形式运行在 Azure VM 中，最初使用安装在同一台 VM 上的 PostgreSQL。</p>
<p>迁移前的架构如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  ▼
</span></span><span style="display:flex;"><span>Azure VM
</span></span><span style="display:flex;"><span>  ├── OpenWebUI Docker 容器
</span></span><span style="display:flex;"><span>  │      └── 连接 VM 宿主机 PostgreSQL
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  └── PostgreSQL
</span></span><span style="display:flex;"><span>         └── 数据保存在 VM 本地磁盘
</span></span></code></pre></div><p>这种部署方式比较简单，但随着应用逐渐稳定运行，数据库维护也成为了新的负担。</p>
<p>需要自行处理的工作包括：</p>
<ul>
<li>PostgreSQL 安全更新和补丁</li>
<li>数据库备份及备份轮换</li>
<li>数据恢复和恢复演练</li>
<li>磁盘容量监控</li>
<li>数据库服务监控</li>
<li>PostgreSQL 版本升级</li>
<li>故障处理</li>
<li>网络和访问权限管理</li>
</ul>
<p>对于 OpenWebUI 这样的应用来说，数据库是重要的状态组件，但并不是应用的核心运维目标。因此，将 PostgreSQL 迁移到 Azure 托管服务是一个更合理的选择。</p>
<h2 id="迁移后的架构">迁移后的架构</h2>
<p>迁移后的架构如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户
</span></span><span style="display:flex;"><span>  │
</span></span><span style="display:flex;"><span>  ▼
</span></span><span style="display:flex;"><span>Azure VM
</span></span><span style="display:flex;"><span>  └── OpenWebUI Docker 容器
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          ├── Private Endpoint
</span></span><span style="display:flex;"><span>          │       └── Azure Database for PostgreSQL
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          └── 宿主机持久化目录
</span></span><span style="display:flex;"><span>                  ├── uploads
</span></span><span style="display:flex;"><span>                  ├── vector_db
</span></span><span style="display:flex;"><span>                  └── cache
</span></span></code></pre></div><p>最终实现了：</p>
<ul>
<li>OpenWebUI 使用 Azure 托管 PostgreSQL</li>
<li>数据库通过 Private Endpoint 私网访问</li>
<li>数据库连接启用 TLS</li>
<li>PostgreSQL 不再占用 VM 本地端口和磁盘</li>
<li>OpenWebUI 上传文件和向量数据实现持久化挂载</li>
<li>Azure 自动执行数据库备份</li>
<li>VM 上只保留 PostgreSQL 客户端，用于日常管理 Azure 数据库</li>
</ul>
<p><img alt="架构" loading="lazy" src="/zh/blog/2026/migrate-openwebui-to-azure-pg/2-architecture.png"></p>
<h2 id="为什么选择-azure-database-for-postgresql">为什么选择 Azure Database for PostgreSQL</h2>
<h3 id="1-降低数据库运维成本">1. 降低数据库运维成本</h3>
<p>Azure Database for PostgreSQL Flexible Server 是一项托管数据库服务。</p>
<p>与 VM 自建 PostgreSQL 相比，Azure 可以帮助处理大量底层工作，例如：</p>
<ul>
<li>数据库基础设施维护</li>
<li>自动备份</li>
<li>事务日志备份</li>
<li>时间点恢复</li>
<li>监控指标采集</li>
<li>存储管理</li>
<li>计划性维护</li>
<li>可选高可用</li>
<li>可选只读副本</li>
</ul>
<p>迁移完成后，VM 只需要负责运行 OpenWebUI，数据库生命周期则交给 Azure 管理。</p>
<h3 id="2-自动备份和时间点恢复">2. 自动备份和时间点恢复</h3>
<p>Azure PostgreSQL 提供内置自动备份，并持续备份事务日志，支持时间点恢复（Point-in-Time Restore，PITR）。</p>
<p>当前环境配置了自动备份保留策略。发生误操作时，可以在保留期内创建一台恢复到指定时间点的新服务器。</p>
<p>与自行编写 <code>pg_dump</code> 和 Cron 脚本相比，Azure 托管备份具有以下优势：</p>
<ul>
<li>不需要维护备份脚本</li>
<li>不需要手动轮换备份文件</li>
<li>不会占用 VM 本地磁盘</li>
<li>可以恢复到保留期内的特定时间</li>
<li>备份操作由平台统一管理</li>
</ul>
<p>需要注意的是，Azure 内置备份主要用于平台恢复，不等同于可随意下载的逻辑备份文件。</p>
<p>如果有以下需求，仍建议额外定期执行 <code>pg_dump</code>：</p>
<ul>
<li>长期离线归档</li>
<li>跨平台恢复</li>
<li>数据审计</li>
<li>跨订阅或跨云迁移</li>
<li>保留期超过平台备份策略</li>
</ul>
<h3 id="3-使用-private-endpoint-私网访问">3. 使用 Private Endpoint 私网访问</h3>
<p>本次迁移没有将 PostgreSQL 直接暴露到公网，而是为 Azure PostgreSQL 配置了 Private Endpoint。</p>
<p>OpenWebUI 访问数据库时，数据库域名通过 Private DNS 解析到 VNet 中的私有 IP：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Azure PostgreSQL FQDN
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>Private DNS
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>Private Endpoint 私有 IP
</span></span></code></pre></div><p>从 OpenWebUI 容器内部进行了两层验证：</p>
<ol>
<li>DNS 是否解析到私有地址</li>
<li>TCP 5432 是否可以通过私网访问</li>
</ol>
<p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker exec -i open-webui python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import socket
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">host = &#34;&lt;AZURE_POSTGRESQL_PRIVATE_FQDN&gt;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">port = 5432
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">addresses = sorted({
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    item[4][0]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    for item in socket.getaddrinfo(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        host,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        port,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        type=socket.SOCK_STREAM
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">})
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Resolved addresses:&#34;, addresses)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">with socket.create_connection((host, port), timeout=5) as sock:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    print(&#34;TCP connection: OK&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    print(&#34;Connected peer:&#34;, sock.getpeername())
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>数据库安全性并不只是来自 Private Endpoint，而是来自以下措施的组合：</p>
<ul>
<li>Private Endpoint</li>
<li>Private DNS</li>
<li>VNet 隔离</li>
<li>NSG 和路由控制</li>
<li>禁用或限制公网访问</li>
<li>PostgreSQL 身份认证</li>
<li>TLS 链路加密</li>
<li>最小权限数据库账号</li>
</ul>
<h3 id="4-数据库连接启用-tls">4. 数据库连接启用 TLS</h3>
<p>OpenWebUI 的数据库连接串启用了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>sslmode=require
</span></span></code></pre></div><p>脱敏后的连接串格式如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>postgresql://&lt;APP_USER&gt;:&lt;PASSWORD&gt;@&lt;AZURE_POSTGRESQL_FQDN&gt;:5432/&lt;APP_DATABASE&gt;?sslmode=require
</span></span></code></pre></div><p>迁移前还通过 OpenWebUI 使用的 Python 和 SQLAlchemy 环境实际验证了 TLS：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> create_engine, text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>engine <span style="color:#f92672">=</span> create_engine(os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;DATABASE_URL&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> engine<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> conn:
</span></span><span style="display:flex;"><span>    ssl_enabled <span style="color:#f92672">=</span> conn<span style="color:#f92672">.</span>execute(text(<span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SELECT ssl
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        FROM pg_stat_ssl
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE pid = pg_backend_pid()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>))<span style="color:#f92672">.</span>scalar_one()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;TLS enabled:&#34;</span>, ssl_enabled)
</span></span></code></pre></div><p>如果安全要求更高，可以进一步使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>sslmode=verify-full
</span></span></code></pre></div><p>这种模式不仅加密连接，还会验证证书和服务器主机名，但需要正确配置 CA 证书，并使用与证书匹配的服务器 FQDN。</p>
<h3 id="5-更容易扩展可靠性能力">5. 更容易扩展可靠性能力</h3>
<p>迁移到 Azure PostgreSQL 后，可以根据 OpenWebUI 的使用规模继续配置：</p>
<ul>
<li>计算规格扩缩容</li>
<li>存储扩容</li>
<li>可用区高可用</li>
<li>只读副本</li>
<li>慢查询分析</li>
<li>数据库连接监控</li>
<li>CPU、内存和存储告警</li>
<li>更长的备份保留期</li>
<li>异地恢复策略</li>
</ul>
<p>这使 OpenWebUI 的数据库层从“单 VM 上的本地服务”升级为独立、可管理的托管数据服务。</p>
<h2 id="迁移目标让-openwebui-尽量保持在线">迁移目标：让 OpenWebUI 尽量保持在线</h2>
<p>传统迁移通常采用以下流程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>停止应用
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>导出数据库
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>恢复目标数据库
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>修改连接配置
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>重新启动应用
</span></span></code></pre></div><p>这种方式虽然简单，但数据库越大，停机时间越长。</p>
<p>本次迁移采用了不同策略：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>在线全量备份
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>在线恢复到 Azure PostgreSQL
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>验证结构、权限和数据
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>同步迁移期间新增的活跃对话
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>快速重建 OpenWebUI 容器
</span></span></code></pre></div><p>在以下阶段，OpenWebUI 都保持在线：</p>
<ul>
<li>全量 <code>pg_dump</code></li>
<li>Azure PostgreSQL 数据恢复</li>
<li>表结构验证</li>
<li>数据量对比</li>
<li>Private Endpoint 验证</li>
<li>TLS 验证</li>
<li>增量数据同步</li>
</ul>
<p>最终只有更换 <code>DATABASE_URL</code> 并重建容器时发生了短暂连接中断。</p>
<p>因此，更准确地说，这是一种：</p>

<blockquote>
  <p><strong>近零停机、用户侧近乎无感知的 OpenWebUI 数据库迁移方案。</strong></p>

</blockquote>
<h2 id="迁移前检查-openwebui">迁移前检查 OpenWebUI</h2>
<h3 id="1-确认-openwebui-使用的数据库类型">1. 确认 OpenWebUI 使用的数据库类型</h3>
<p>首先检查 OpenWebUI 容器环境变量：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect open-webui <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format <span style="color:#e6db74">&#39;{{range .Config.Env}}{{println .}}{{end}}&#39;</span> |
</span></span><span style="display:flex;"><span>grep -E <span style="color:#e6db74">&#39;DATABASE_URL|DATA_DIR|WEBUI_SECRET_KEY&#39;</span>
</span></span></code></pre></div><p>确认 OpenWebUI 已经使用 PostgreSQL，而不是默认 SQLite：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DATABASE_URL=postgresql://&lt;LOCAL_USER&gt;:***@&lt;LOCAL_HOST&gt;:5432/&lt;LOCAL_DATABASE&gt;
</span></span></code></pre></div><p>由于源端和目标端都是 PostgreSQL，因此可以直接使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>pg_dump → pg_restore
</span></span></code></pre></div><p>不需要使用 SQLite 到 PostgreSQL 的转换工具。</p>
<h3 id="2-检查源数据库">2. 检查源数据库</h3>
<p>检查 PostgreSQL 版本和数据库大小：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d &lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  current_database(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  current_setting(&#39;server_version&#39;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  pg_size_pretty(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    pg_database_size(current_database())
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  );
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>同时检查源数据库使用了哪些扩展：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d &lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT extname, extversion
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">FROM pg_extension
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ORDER BY extname;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>本次源数据库没有使用额外的第三方扩展，因此降低了跨环境迁移的兼容性风险。</p>
<h3 id="3-检查-openwebui-的本地文件">3. 检查 OpenWebUI 的本地文件</h3>
<p>数据库并不是 OpenWebUI 的全部数据。</p>
<p>检查容器挂载后发现，原容器没有配置持久化 volume，而 <code>/app/backend/data</code> 中已经存在大量数据：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/app/backend/data
</span></span><span style="display:flex;"><span>├── cache
</span></span><span style="display:flex;"><span>├── uploads
</span></span><span style="display:flex;"><span>└── vector_db
</span></span></code></pre></div><p>这些内容包括：</p>
<ul>
<li>用户上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>其他应用本地状态</li>
</ul>
<p>它们不会进入 PostgreSQL 的 <code>pg_dump</code>，因此必须单独处理。</p>
<p>这也是本次迁移中一个非常重要的发现：</p>

<blockquote>
  <p>迁移 OpenWebUI 的 PostgreSQL，并不等于迁移了 OpenWebUI 的全部数据。</p>

</blockquote>
<h2 id="准备-azure-postgresql">准备 Azure PostgreSQL</h2>
<h3 id="1-创建独立应用账号">1. 创建独立应用账号</h3>
<p>不建议让 OpenWebUI 长期使用 Azure PostgreSQL 管理员账号。</p>
<p>在目标服务器中创建专用角色：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">ROLE</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span> LOGIN;
</span></span></code></pre></div><p>通过 <code>psql</code> 交互方式设置密码，可以避免密码出现在 Shell 历史记录中：</p>
<pre tabindex="0"><code class="language-psql" data-lang="psql">\password &lt;APP_USER&gt;
</code></pre><p>然后创建目标数据库：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">DATABASE</span> <span style="color:#f92672">&lt;</span>APP_DATABASE<span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">OWNER</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span></code></pre></div><h3 id="2-验证应用账号">2. 验证应用账号</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;host=&lt;AZURE_POSTGRESQL_FQDN&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   port=5432 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   dbname=&lt;APP_DATABASE&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   user=&lt;APP_USER&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   sslmode=require&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -W <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;SELECT current_user, current_database();&#34;</span>
</span></span></code></pre></div><p>同时确认目标数据库为空：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> information_schema.tables
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> table_schema <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> table_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;BASE TABLE&#39;</span>;
</span></span></code></pre></div><h3 id="3-处理-public-schema-权限">3. 处理 <code>public</code> schema 权限</h3>
<p>第一次测试恢复时遇到了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ERROR: permission denied for schema public
</span></span></code></pre></div><p>原因是应用账号虽然可以连接数据库，但没有在 <code>public</code> schema 中创建对象的权限。</p>
<p>使用管理员账号连接目标数据库后执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">SCHEMA</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">OWNER</span> <span style="color:#66d9ef">TO</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GRANT</span> <span style="color:#66d9ef">USAGE</span>, <span style="color:#66d9ef">CREATE</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">SCHEMA</span> <span style="color:#66d9ef">public</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">TO</span> <span style="color:#f92672">&lt;</span>APP_USER<span style="color:#f92672">&gt;</span>;
</span></span></code></pre></div><p>这个问题说明，仅验证账号“能否登录”还不够，还需要确认该账号能否：</p>
<ul>
<li>创建表</li>
<li>创建索引</li>
<li>创建序列</li>
<li>创建约束</li>
<li>执行 OpenWebUI 的 Alembic 数据库迁移</li>
</ul>
<h2 id="在线执行全量数据库迁移">在线执行全量数据库迁移</h2>
<h3 id="1-在线导出-openwebui-数据库">1. 在线导出 OpenWebUI 数据库</h3>
<p>PostgreSQL 的 <code>pg_dump</code> 会基于一致性事务快照导出数据，因此可以在 OpenWebUI 继续运行时执行。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres pg_dump <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --dbname<span style="color:#f92672">=</span>&lt;LOCAL_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format<span style="color:#f92672">=</span>custom <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-owner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-acl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --file<span style="color:#f92672">=</span>/tmp/openwebui-pre-migration.dump
</span></span></code></pre></div><p>参数含义：</p>
<ul>
<li><code>--format=custom</code>：使用 PostgreSQL 自定义归档格式</li>
<li><code>--no-owner</code>：不恢复源端对象 owner</li>
<li><code>--no-acl</code>：不恢复源端权限</li>
<li><code>--file</code>：指定备份文件</li>
</ul>
<p>检查归档是否有效：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pg_restore <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --list <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /tmp/openwebui-pre-migration.dump |
</span></span><span style="display:flex;"><span>head -30
</span></span></code></pre></div><p>自定义格式会保留：</p>
<ul>
<li>表结构</li>
<li>表数据</li>
<li>主键</li>
<li>唯一约束</li>
<li>索引</li>
<li>外键</li>
<li>序列</li>
<li>Alembic 迁移版本</li>
</ul>
<h3 id="2-恢复到-azure-postgresql">2. 恢复到 Azure PostgreSQL</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pg_restore <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --host<span style="color:#f92672">=</span>&lt;AZURE_POSTGRESQL_FQDN&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --port<span style="color:#f92672">=</span><span style="color:#ae81ff">5432</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --username<span style="color:#f92672">=</span>&lt;APP_USER&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --dbname<span style="color:#f92672">=</span>&lt;APP_DATABASE&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-owner <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --no-acl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --exit-on-error <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --verbose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /tmp/openwebui-pre-migration.dump
</span></span></code></pre></div><p>使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>--exit-on-error
</span></span></code></pre></div><p>可以确保遇到恢复错误时立即停止，避免忽略关键问题。</p>
<p>恢复成功后更新统计信息：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>psql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;host=&lt;AZURE_POSTGRESQL_FQDN&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   port=5432 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   dbname=&lt;APP_DATABASE&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   user=&lt;APP_USER&gt; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   sslmode=require&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -W <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -c <span style="color:#e6db74">&#34;ANALYZE;&#34;</span>
</span></span></code></pre></div><h2 id="对比源端与目标端数据">对比源端与目标端数据</h2>
<p>恢复完成后，需要对 OpenWebUI 的核心表进行对比。</p>
<p>示例查询：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">&#34;user&#34;</span>) <span style="color:#66d9ef">AS</span> users,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> chat) <span style="color:#66d9ef">AS</span> chats,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> chat_message) <span style="color:#66d9ef">AS</span> chat_messages,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> file) <span style="color:#66d9ef">AS</span> files,
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> knowledge) <span style="color:#66d9ef">AS</span> knowledge,
</span></span><span style="display:flex;"><span>  (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">FROM</span> information_schema.tables
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> table_schema <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">AND</span> table_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;BASE TABLE&#39;</span>
</span></span><span style="display:flex;"><span>  ) <span style="color:#66d9ef">AS</span> tables;
</span></span></code></pre></div><p>重点检查：</p>
<ul>
<li>用户数量</li>
<li>对话数量</li>
<li>消息数量</li>
<li>文件记录数量</li>
<li>知识库数量</li>
<li>业务表数量</li>
<li>Alembic 版本</li>
<li>表 owner</li>
<li>序列状态</li>
</ul>
<p>对比时发现，目标端的对话消息比源端少了少量记录。</p>
<p>这并不是恢复失败，而是因为：</p>

<blockquote>
  <p>在全量备份和恢复过程中，OpenWebUI 仍然在线，当前迁移操作的对话还在不断产生新消息。</p>

</blockquote>
<p>因此，下一步需要同步全量快照之后新增的数据。</p>
<h2 id="同步迁移期间新增的活跃对话">同步迁移期间新增的活跃对话</h2>
<p>本次迁移期间，主要只有当前操作对话仍在更新，因此没有再次恢复整个数据库，而是只同步当前活跃对话。</p>
<p>涉及两张表：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>chat
</span></span><span style="display:flex;"><span>chat_message
</span></span></code></pre></div><h3 id="1-找到最近更新的对话">1. 找到最近更新的对话</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.id,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.title,
</span></span><span style="display:flex;"><span>  to_timestamp(<span style="color:#66d9ef">c</span>.updated_at) <span style="color:#66d9ef">AS</span> updated_at,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">count</span>(cm.id) <span style="color:#66d9ef">AS</span> message_count
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> chat <span style="color:#66d9ef">c</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> chat_message cm
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">ON</span> cm.chat_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">c</span>.id
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GROUP</span> <span style="color:#66d9ef">BY</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.id,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.title,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">c</span>.updated_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">c</span>.updated_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">5</span>;
</span></span></code></pre></div><p>确定活跃对话后，后续脚本通过环境变量或参数接收对话 ID，而不是将其直接写入公开文档：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>CHAT_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;ACTIVE_CHAT_ID&gt;&#34;</span>
</span></span></code></pre></div><h3 id="2-使用一致性快照导出对话数据">2. 使用一致性快照导出对话数据</h3>
<p>为了保证 <code>chat</code> 和 <code>chat_message</code> 来自同一个时间点，使用只读、可重复读事务：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span> <span style="color:#66d9ef">TRANSACTION</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ISOLATION</span> <span style="color:#66d9ef">LEVEL</span> <span style="color:#66d9ef">REPEATABLE</span> <span style="color:#66d9ef">READ</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">READ</span> <span style="color:#66d9ef">ONLY</span>;
</span></span></code></pre></div><p>然后在同一事务中导出：</p>
<pre tabindex="0"><code class="language-psql" data-lang="psql">\copy (
  SELECT *
  FROM chat
  WHERE id = &#39;&lt;ACTIVE_CHAT_ID&gt;&#39;
) TO &#39;/tmp/chat.csv&#39;
WITH (FORMAT CSV);
</code></pre><pre tabindex="0"><code class="language-psql" data-lang="psql">\copy (
  SELECT *
  FROM chat_message
  WHERE chat_id = &#39;&lt;ACTIVE_CHAT_ID&gt;&#39;
  ORDER BY created_at, id
) TO &#39;/tmp/chat-messages.csv&#39;
WITH (FORMAT CSV);
</code></pre><p>最后提交：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></div><p>需要注意：<code>psql</code> 的 <code>\copy</code> 是客户端元命令，复杂字段列表最好写在同一行，否则可能出现：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>\copy: parse error at end of line
</span></span></code></pre></div><h3 id="3-在-azure-中使用临时表和-upsert">3. 在 Azure 中使用临时表和 UPSERT</h3>
<p>目标端创建临时 staging 表：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> TEMP <span style="color:#66d9ef">TABLE</span> stage_chat
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">LIKE</span> <span style="color:#66d9ef">public</span>.chat <span style="color:#66d9ef">INCLUDING</span> <span style="color:#66d9ef">DEFAULTS</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> TEMP <span style="color:#66d9ef">TABLE</span> stage_chat_message
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">LIKE</span> <span style="color:#66d9ef">public</span>.chat_message <span style="color:#66d9ef">INCLUDING</span> <span style="color:#66d9ef">DEFAULTS</span>);
</span></span></code></pre></div><p>将 CSV 导入临时表后，对 <code>chat</code> 主记录执行 UPSERT：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.chat (...)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> stage_chat
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> CONFLICT (id) <span style="color:#66d9ef">DO</span> <span style="color:#66d9ef">UPDATE</span> <span style="color:#66d9ef">SET</span>
</span></span><span style="display:flex;"><span>  title              <span style="color:#f92672">=</span> EXCLUDED.title,
</span></span><span style="display:flex;"><span>  updated_at         <span style="color:#f92672">=</span> EXCLUDED.updated_at,
</span></span><span style="display:flex;"><span>  chat               <span style="color:#f92672">=</span> EXCLUDED.chat,
</span></span><span style="display:flex;"><span>  meta               <span style="color:#f92672">=</span> EXCLUDED.meta,
</span></span><span style="display:flex;"><span>  current_message_id <span style="color:#f92672">=</span> EXCLUDED.current_message_id;
</span></span></code></pre></div><p>对消息表执行类似操作：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">public</span>.chat_message (...)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> ...
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> stage_chat_message
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> CONFLICT (id) <span style="color:#66d9ef">DO</span> <span style="color:#66d9ef">UPDATE</span> <span style="color:#66d9ef">SET</span>
</span></span><span style="display:flex;"><span>  content         <span style="color:#f92672">=</span> EXCLUDED.content,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">output</span>          <span style="color:#f92672">=</span> EXCLUDED.<span style="color:#66d9ef">output</span>,
</span></span><span style="display:flex;"><span>  status_history  <span style="color:#f92672">=</span> EXCLUDED.status_history,
</span></span><span style="display:flex;"><span>  error           <span style="color:#f92672">=</span> EXCLUDED.error,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">usage</span>           <span style="color:#f92672">=</span> EXCLUDED.<span style="color:#66d9ef">usage</span>,
</span></span><span style="display:flex;"><span>  done            <span style="color:#f92672">=</span> EXCLUDED.done,
</span></span><span style="display:flex;"><span>  updated_at      <span style="color:#f92672">=</span> EXCLUDED.updated_at,
</span></span><span style="display:flex;"><span>  context_summary <span style="color:#f92672">=</span> EXCLUDED.context_summary;
</span></span></code></pre></div><p>为了让 Azure 中该对话的消息集合与源快照完全一致，还会删除目标端多余记录：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> <span style="color:#66d9ef">public</span>.chat_message target
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> target.chat_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&lt;ACTIVE_CHAT_ID&gt;&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SELECT</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">FROM</span> stage_chat_message staged
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> staged.id <span style="color:#f92672">=</span> target.id
</span></span><span style="display:flex;"><span>  );
</span></span></code></pre></div><p>整个导入和 UPSERT 过程放在一个事务中执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- 创建临时表
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- 导入数据
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- UPSERT
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- 清理多余记录
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></div><p>如果任何步骤失败，事务会自动回滚，不会留下部分同步数据。</p>
<h2 id="最终快速切换-openwebui">最终快速切换 OpenWebUI</h2>
<p>完成在线全量迁移后，最终操作为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>最后同步一次当前活跃对话
</span></span><span style="display:flex;"><span>              ↓
</span></span><span style="display:flex;"><span>重建 OpenWebUI 容器
</span></span><span style="display:flex;"><span>              ↓
</span></span><span style="display:flex;"><span>新容器连接 Azure PostgreSQL
</span></span></code></pre></div><p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./sync-active-chat-to-azure.sh <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  up -d --force-recreate
</span></span></code></pre></div><p>完整数据库已经提前迁移并验证，因此最终阶段不需要再次等待几百 MB 数据恢复，只需要同步少量增量记录并快速重建容器。</p>
<p>这将切换窗口缩短到了容器启动所需的时间。</p>
<h2 id="安全保存-openwebui-数据库连接串">安全保存 OpenWebUI 数据库连接串</h2>
<p>数据库连接串没有直接写入 Compose 文件，而是放在独立环境文件中：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>open-webui-azure-db.env
</span></span></code></pre></div><p>文件内容形式：</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">DATABASE_URL=postgresql://&lt;APP_USER&gt;:&lt;URL_ENCODED_PASSWORD&gt;@&lt;AZURE_POSTGRESQL_FQDN&gt;:5432/&lt;APP_DATABASE&gt;?sslmode=require
</code></pre><p>文件权限设置为：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chmod <span style="color:#ae81ff">600</span> open-webui-azure-db.env
</span></span></code></pre></div><p>如果密码包含特殊字符，需要先进行 URL 编码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import getpass
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import urllib.parse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">password = getpass.getpass(&#34;Database password: &#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(urllib.parse.quote(password, safe=&#34;&#34;))
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>新的 Compose 文件示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">open-webui</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/open-webui/open-webui:&lt;VERSION&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">open-webui</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8080:8080&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env_file</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./open-webui.env</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./open-webui-azure-db.env</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/opt/open-webui/data:/app/backend/data</span>
</span></span></code></pre></div><h2 id="持久化-openwebui-本地文件">持久化 OpenWebUI 本地文件</h2>
<p>迁移前，OpenWebUI 的 <code>/app/backend/data</code> 没有挂载到宿主机。</p>
<p>先将数据复制出来：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo install <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -m <span style="color:#ae81ff">700</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>id -un<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -g <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>id -gn<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /opt/open-webui/data
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker cp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  open-webui:/app/backend/data/. <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /opt/open-webui/data/
</span></span></code></pre></div><p>在 Compose 中加入：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">/opt/open-webui/data:/app/backend/data</span>
</span></span></code></pre></div><p>这样，即使未来重建或升级 OpenWebUI 容器，以下内容仍会保留：</p>
<ul>
<li>上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>其他本地应用数据</li>
</ul>
<p>迁移后验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect open-webui <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --format <span style="color:#e6db74">&#39;{{range .Mounts}}{{println .Source &#34;-&gt;&#34; .Destination &#34;rw=&#34; .RW}}{{end}}&#39;</span>
</span></span></code></pre></div><p>预期：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/opt/open-webui/data -&gt; /app/backend/data rw=true
</span></span></code></pre></div><h2 id="迁移后的验证">迁移后的验证</h2>
<h3 id="1-openwebui-健康检查">1. OpenWebUI 健康检查</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -sS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o /dev/null <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -w <span style="color:#e6db74">&#39;HTTP %{http_code}\n&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --max-time <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  http://127.0.0.1:8080/health
</span></span></code></pre></div><p>预期：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>HTTP 200
</span></span></code></pre></div><h3 id="2-验证-openwebui-当前数据库">2. 验证 OpenWebUI 当前数据库</h3>
<p>为了避免输出密码，只解析连接串中的非敏感部分：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker exec -i open-webui python3 - <span style="color:#e6db74">&lt;&lt;&#39;PY&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">import os
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">from urllib.parse import urlsplit
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">u = urlsplit(os.environ[&#34;DATABASE_URL&#34;])
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Database host configured:&#34;, bool(u.hostname))
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;Database port:&#34;, u.port)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">print(&#34;TLS required:&#34;, &#34;sslmode=require&#34; in u.query)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PY</span>
</span></span></code></pre></div><p>验证重点：</p>
<ul>
<li>Host 已不再指向 VM 本地 PostgreSQL</li>
<li>端口为 5432</li>
<li>已启用 <code>sslmode=require</code></li>
</ul>
<h3 id="3-检查启动日志">3. 检查启动日志</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker logs <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --since 15m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  open-webui 2&gt;&amp;<span style="color:#ae81ff">1</span> |
</span></span><span style="display:flex;"><span>grep -Ei <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;error|fatal|exception|traceback|permission denied|database|postgres|alembic|ssl|timeout&#39;</span>
</span></span></code></pre></div><p>正常情况下只会看到类似：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO [alembic.runtime.migration] Will assume transactional DDL.
</span></span></code></pre></div><p>不应出现：</p>
<ul>
<li>数据库连接失败</li>
<li>密码认证失败</li>
<li>TLS 错误</li>
<li>Schema 权限错误</li>
<li>Alembic 迁移失败</li>
<li>Python Traceback</li>
</ul>
<h2 id="卸载-vm-本地-postgresql">卸载 VM 本地 PostgreSQL</h2>
<p>确认 OpenWebUI 已稳定连接 Azure PostgreSQL 后，可以删除 VM 上的本地 PostgreSQL。</p>
<p>首先确认：</p>
<ul>
<li>OpenWebUI 健康检查正常</li>
<li>数据库连接已切换</li>
<li>Azure PostgreSQL 中存在最新数据</li>
<li>自动备份策略已经启用</li>
<li>本地 PostgreSQL 不再有应用连接</li>
</ul>
<p>停止并删除本地集群：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo pg_dropcluster <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --stop <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  &lt;POSTGRESQL_VERSION&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  main
</span></span></code></pre></div><p>卸载 PostgreSQL 服务端：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get purge -y <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-&lt;POSTGRESQL_VERSION&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  postgresql-contrib
</span></span></code></pre></div><p>删除遗留数据和配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo rm -rf <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/lib/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /etc/postgresql <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  /var/log/postgresql
</span></span></code></pre></div><p>可以继续保留 PostgreSQL 客户端：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>postgresql-client
</span></span><span style="display:flex;"><span>postgresql-client-common
</span></span></code></pre></div><p>这样仍然可以从 VM 使用 <code>psql</code> 管理 Azure PostgreSQL。</p>
<p>最终检查本地 5432：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> sudo ss -lntp | grep -q <span style="color:#e6db74">&#39;:5432&#39;</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  sudo ss -lntp | grep <span style="color:#e6db74">&#39;:5432&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;No local PostgreSQL listener&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><h2 id="迁移过程中的经验总结">迁移过程中的经验总结</h2>
<h3 id="1-数据库迁移不等于-openwebui-全部数据迁移">1. 数据库迁移不等于 OpenWebUI 全部数据迁移</h3>
<p>OpenWebUI 除了 PostgreSQL，还可能在 <code>/app/backend/data</code> 中保存：</p>
<ul>
<li>上传文件</li>
<li>向量数据库</li>
<li>缓存</li>
<li>本地状态文件</li>
</ul>
<p>只执行 <code>pg_dump</code> 并不能迁移这些内容。</p>
<h3 id="2-不要只测试端口连通">2. 不要只测试端口连通</h3>
<p><code>nc</code> 成功只能证明 TCP 5432 可达，并不能证明：</p>
<ul>
<li>PostgreSQL 用户可以登录</li>
<li>密码正确</li>
<li>TLS 正常</li>
<li>数据库权限正确</li>
<li>OpenWebUI 使用的驱动兼容</li>
</ul>
<p>应至少完成以下验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DNS → TCP → PostgreSQL 登录 → TLS → SQL 查询 → OpenWebUI 实际连接
</span></span></code></pre></div><h3 id="3-数据库-owner-不代表一定拥有-schema-创建权限">3. 数据库 owner 不代表一定拥有 schema 创建权限</h3>
<p>即使应用用户是数据库 owner，也可能遇到：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>permission denied for schema public
</span></span></code></pre></div><p>迁移前应验证应用账号是否具备：</p>
<ul>
<li><code>USAGE</code></li>
<li><code>CREATE</code></li>
<li>表和序列所有权</li>
<li>Alembic DDL 权限</li>
</ul>
<h3 id="4-在线全量备份后必须处理增量写入">4. 在线全量备份后必须处理增量写入</h3>
<p><code>pg_dump</code> 提供的是开始备份时的一致性快照。</p>
<p>备份期间 OpenWebUI 新产生的对话和消息不会自动进入已经完成的 dump。因此在线迁移必须增加以下机制之一：</p>
<ul>
<li>最终停写后重新完整备份</li>
<li>逻辑复制</li>
<li>CDC</li>
<li>应用双写</li>
<li>按业务对象执行增量同步</li>
</ul>
<p>本次采用的是“全量快照 + 活跃对话 UPSERT”。</p>
<h3 id="5-不要在迁移时顺便升级-openwebui">5. 不要在迁移时顺便升级 OpenWebUI</h3>
<p>本次迁移始终保持 OpenWebUI 镜像版本不变。</p>
<p>如果同时升级应用和迁移数据库，一旦发生问题，很难快速判断原因来自：</p>
<ul>
<li>PostgreSQL 版本</li>
<li>数据迁移</li>
<li>Azure 网络</li>
<li>数据库权限</li>
<li>OpenWebUI 版本</li>
<li>Alembic schema 变化</li>
</ul>
<p>更稳妥的做法是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>先迁移数据库并稳定运行
</span></span><span style="display:flex;"><span>          ↓
</span></span><span style="display:flex;"><span>再单独升级 OpenWebUI
</span></span></code></pre></div><h2 id="这算不算真正的零停机">这算不算真正的零停机？</h2>
<p>从用户体验看，这次迁移接近零停机：</p>
<ul>
<li>在线完成全量备份</li>
<li>在线恢复目标数据库</li>
<li>在线完成数据验证</li>
<li>在线同步活跃对话</li>
<li>仅在最后重建了一次 OpenWebUI 容器</li>
</ul>
<p>但如果采用严格工程定义，真正的零停机还需要满足：</p>
<ul>
<li>任意时刻都能接受新连接</li>
<li>最后时刻写入绝不丢失</li>
<li>没有单实例容器重启窗口</li>
<li>客户端连接完全不中断</li>
<li>具备自动流量切换能力</li>
</ul>
<p>更大规模的 OpenWebUI 部署可以考虑：</p>
<ul>
<li>PostgreSQL 逻辑复制</li>
<li>Azure Database Migration Service</li>
<li>CDC 增量迁移</li>
<li>应用双写</li>
<li>蓝绿部署</li>
<li>多个 OpenWebUI 实例</li>
<li>反向代理健康检查</li>
<li>连接排空和流量切换</li>
</ul>
<p>对于单 VM、单 OpenWebUI 容器和中小规模数据库，本次采用的方案在复杂度、风险和停机时间之间取得了较好的平衡。</p>
<h2 id="迁移后的日常运维">迁移后的日常运维</h2>
<p>启动 OpenWebUI：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  up -d
</span></span></code></pre></div><p>查看状态：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  ps
</span></span></code></pre></div><p>查看日志：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -f run-with-azure-pg.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  logs -f --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span>
</span></span></code></pre></div><p>健康检查：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -sS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o /dev/null <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -w <span style="color:#e6db74">&#39;HTTP %{http_code}\n&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  http://127.0.0.1:8080/health
</span></span></code></pre></div><p>日常还应重点监控 Azure PostgreSQL 的：</p>
<ul>
<li>CPU 使用率</li>
<li>内存使用情况</li>
<li>存储空间</li>
<li>活跃连接数</li>
<li>失败连接数</li>
<li>查询延迟</li>
<li>自动备份状态</li>
<li>维护事件</li>
<li>Private Endpoint 状态</li>
</ul>
<h2 id="总结">总结</h2>
<p>这次迁移将 OpenWebUI 的数据库从 Azure VM 自建 PostgreSQL 平滑迁移到了 Azure Database for PostgreSQL Flexible Server。</p>
<p>最终实现了：</p>
<ul>
<li>OpenWebUI 数据库由 Azure 托管</li>
<li>通过 Private Endpoint 私网访问</li>
<li>数据库连接启用 TLS</li>
<li>自动备份和时间点恢复</li>
<li>OpenWebUI 本地文件持久化</li>
<li>VM 本地 PostgreSQL 完全下线</li>
<li>本地 5432 端口关闭</li>
<li>全量迁移期间应用持续在线</li>
<li>最终切换对用户影响极低</li>
</ul>
<p>本次最关键的经验是：</p>

<blockquote>
  <p>对 OpenWebUI 进行低停机数据库迁移，不应只关注 <code>pg_dump</code> 和 <code>pg_restore</code>，还必须同时考虑增量写入、Private Endpoint、TLS、数据库权限、Alembic 迁移以及 <code>/app/backend/data</code> 中的本地文件。</p>

</blockquote>
<p>通过“<strong>在线全量迁移、目标端预验证、活跃数据增量同步、快速容器切换</strong>”，可以在不引入复杂 CDC 平台的情况下，将中小规模 OpenWebUI 平滑迁移到 Azure 托管 PostgreSQL。</p>
]]></content:encoded></item><item><title>用 llm-d-inference-sim 模拟大模型推理：无 GPU 环境下的最佳验证工具</title><link>https://wilsonwu.me/zh/blog/2026/llm-inference-simulation/</link><pubDate>Wed, 08 Jul 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/llm-inference-simulation/</guid><description>&lt;p&gt;&lt;img alt="LLM 推理模拟" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/llm-inference-simulation/1-llm-inference-simulation.png"&gt;&lt;/p&gt;
&lt;p&gt;在大模型应用快速落地的过程中，&lt;strong&gt;推理服务的工程化能力&lt;/strong&gt;往往比模型本身更复杂：模型调度、路由策略、限流、灰度发布、网关治理等问题，都需要真实的推理接口进行验证。然而，大模型推理依赖 GPU 和高成本算力资源，使得开发和测试门槛居高不下。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="LLM 推理模拟" loading="lazy" src="/zh/blog/2026/llm-inference-simulation/1-llm-inference-simulation.png"></p>
<p>在大模型应用快速落地的过程中，<strong>推理服务的工程化能力</strong>往往比模型本身更复杂：模型调度、路由策略、限流、灰度发布、网关治理等问题，都需要真实的推理接口进行验证。然而，大模型推理依赖 GPU 和高成本算力资源，使得开发和测试门槛居高不下。</p>
<p>如果你希望在<strong>没有 GPU 或资源受限的环境中验证推理架构设计</strong>，那么这个工具值得关注：</p>
<p><a href="https://github.com/llm-d/llm-d-inference-sim?utm_source=chatgpt.com">llm-d-inference-sim GitHub 项目</a></p>
<p>本文将系统介绍这个工具的能力、设计思路，以及它在实际工程中的应用价值。</p>
<h2 id="工具定位为推理工程而生的模拟器">工具定位：为推理工程而生的“模拟器”</h2>
<p><strong>llm-d-inference-sim</strong> 是一个专门用于模拟大模型推理服务的工具，其核心能力包括：</p>
<ul>
<li>提供与真实 LLM 推理服务一致的接口（如 OpenAI-style API）</li>
<li>接收推理请求（prompt / messages）</li>
<li>按配置生成“模拟响应”</li>
<li>支持多实例、多模型模拟部署</li>
</ul>
<p>简单来说，它并不真正执行模型推理，而是：</p>

<blockquote>
  <p><strong>模拟“推理服务行为”，而非“推理计算本身”</strong></p>

</blockquote>
<h2 id="为什么需要推理模拟">为什么需要推理模拟？</h2>
<p>在真实项目中，我们经常遇到以下问题：</p>
<h3 id="1-推理资源昂贵">1. 推理资源昂贵</h3>
<ul>
<li>GPU 成本高（A100 / H100）</li>
<li>推理服务部署复杂（vLLM / TensorRT-LLM / SGLang）</li>
<li>测试环境难以复刻生产规模</li>
</ul>
<h3 id="2-工程验证依赖真实服务">2. 工程验证依赖真实服务</h3>
<p>很多关键能力必须依赖推理接口验证，例如：</p>
<ul>
<li>API 网关路由策略（Envoy / Istio）</li>
<li>多模型调度与 fallback 机制</li>
<li>embedding + intent routing</li>
<li>限流 / 熔断 / 超时控制</li>
<li>灰度发布与 A/B testing</li>
</ul>
<p>但这些能力<strong>并不依赖模型“正确性”</strong>，只依赖：</p>

<blockquote>
  <p>“是否有一个行为类似 LLM 的服务存在”</p>

</blockquote>
<h2 id="llm-d-inference-sim-的核心价值">llm-d-inference-sim 的核心价值</h2>
<h3 id="1-零-gpu-依赖">1. 零 GPU 依赖</h3>
<p>开发者可以在以下环境直接使用：</p>
<ul>
<li>本地 MacBook</li>
<li>CI/CD 环境</li>
<li>Kubernetes 测试集群</li>
<li>云主机（无 GPU）</li>
</ul>
<p>无需部署任何真实模型。</p>
<h3 id="2-高度可控的响应模拟">2. 高度可控的响应模拟</h3>
<p>可以模拟：</p>
<ul>
<li>固定响应（用于稳定测试）</li>
<li>延迟（模拟推理耗时）</li>
<li>Token 生成节奏（Streaming）</li>
<li>错误返回（测试容错）</li>
</ul>
<p>这使它非常适合：</p>

<blockquote>
  <p><strong>压测、容错测试、链路验证</strong></p>

</blockquote>
<h3 id="3-支持多模型实例">3. 支持多模型实例</h3>
<p>你可以快速部署多个“模拟模型”：</p>
<ul>
<li>gpt-4 模拟服务</li>
<li>embedding 模型模拟</li>
<li>不同延迟 / 不同性能 profile</li>
</ul>
<p>用于验证：</p>
<ul>
<li>多模型路由策略</li>
<li>智能网关（AI Gateway）</li>
<li>fallback 逻辑</li>
</ul>
<h3 id="4-与真实-api-兼容">4. 与真实 API 兼容</h3>
<p>接口风格通常兼容：</p>
<ul>
<li>OpenAI API（/v1/chat/completions）</li>
<li>streaming 输出</li>
</ul>
<p>这意味着：</p>

<blockquote>
  <p><strong>可以无缝替换真实模型服务</strong></p>

</blockquote>
<h2 id="典型使用场景">典型使用场景</h2>
<h3 id="场景-1ai-网关--服务网格验证">场景 1：AI 网关 / 服务网格验证</h3>
<p>例如使用 Envoy / Istio 构建 AI Gateway：</p>
<ul>
<li>基于路径或 header 路由不同模型</li>
<li>embedding-based routing</li>
<li>Canary 发布</li>
</ul>
<p>问题：没有真实模型怎么办？</p>
<p>使用 llm-d-inference-sim 模拟多个模型后端，即可验证完整链路。</p>
<h3 id="场景-2多模型调度系统开发">场景 2：多模型调度系统开发</h3>
<p>在做类似以下系统时：</p>
<ul>
<li>vLLM-aware scheduler</li>
<li>KV Cache aware routing</li>
<li>模型负载均衡</li>
</ul>
<p>你需要：</p>
<ul>
<li>多个模型 endpoint</li>
<li>不同响应延迟</li>
<li>可控行为</li>
</ul>
<p>模拟器可以快速构造这些环境。</p>
<h3 id="场景-3cicd-自动化测试">场景 3：CI/CD 自动化测试</h3>
<p>真实模型服务难以接入 CI：</p>
<ul>
<li>成本高</li>
<li>不稳定</li>
<li>启动慢</li>
</ul>
<p>使用模拟器：</p>
<ul>
<li>启动快速</li>
<li>行为可预测</li>
<li>支持 mock response</li>
</ul>
<p>非常适合集成测试（integration test）</p>
<h3 id="场景-4产品演示--demo-环境">场景 4：产品演示 / Demo 环境</h3>
<p>在售前或内部 Demo 中：</p>
<ul>
<li>不需要真实模型</li>
<li>只需要“看起来能跑”</li>
</ul>
<p>使用模拟器可以：</p>
<ul>
<li>快速部署多个模型服务</li>
<li>构造稳定演示环境</li>
<li>避免 GPU 成本</li>
</ul>
<h2 id="工作原理简析">工作原理简析</h2>
<p>llm-d-inference-sim 的设计可以理解为：</p>
<h3 id="请求流程">请求流程</h3>
<ol>
<li>接收 HTTP 请求（如 <code>/v1/chat/completions</code>）</li>
<li>解析输入（prompt/messages）</li>
<li>根据配置规则生成响应</li>
<li>返回 JSON 或 streaming 输出</li>
</ol>
<h3 id="模拟能力通常包括">模拟能力通常包括</h3>
<ul>
<li>
<p><strong>Response 模板</strong></p>
<ul>
<li>固定文本</li>
<li>参数化生成</li>
</ul>
</li>
<li>
<p><strong>Latency 注入</strong></p>
<ul>
<li>模拟推理耗时（如 200ms / 2s）</li>
</ul>
</li>
<li>
<p><strong>Streaming</strong></p>
<ul>
<li>分块返回 token</li>
<li>模拟真实 LLM 输出节奏</li>
</ul>
</li>
<li>
<p><strong>错误注入</strong></p>
<ul>
<li>500 / timeout</li>
<li>测试重试机制</li>
</ul>
</li>
</ul>
<h2 id="与真实推理服务的关系">与真实推理服务的关系</h2>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>模拟器</th>
					<th>真实模型（vLLM 等）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>GPU</td>
					<td>❌ 不需要</td>
					<td>✅ 必需</td>
			</tr>
			<tr>
					<td>成本</td>
					<td>极低</td>
					<td>高</td>
			</tr>
			<tr>
					<td>响应真实性</td>
					<td>❌ 无语义能力</td>
					<td>✅</td>
			</tr>
			<tr>
					<td>接口兼容性</td>
					<td>✅</td>
					<td>✅</td>
			</tr>
			<tr>
					<td>工程验证能力</td>
					<td>✅</td>
					<td>✅</td>
			</tr>
	</tbody>
</table>

<blockquote>
  <p><strong>模拟器 ≠ 替代模型，而是补齐工程验证环节</strong></p>

</blockquote>
<h2 id="最佳实践建议">最佳实践建议</h2>
<h3 id="1-与网关结合使用">1. 与网关结合使用</h3>
<p>推荐组合：</p>
<ul>
<li>Envoy / Istio</li>
<li>AI Gateway</li>
<li>llm-d-inference-sim</li>
</ul>
<p>构建完整推理流量链路</p>
<h3 id="2-构造多模型拓扑">2. 构造多模型拓扑</h3>
<p>例如：</p>
<ul>
<li>model-a（低延迟）</li>
<li>model-b（高质量，高延迟）</li>
<li>model-fallback</li>
</ul>
<p>验证智能路由策略</p>
<h3 id="3-注入异常场景">3. 注入异常场景</h3>
<p>主动模拟：</p>
<ul>
<li>timeout</li>
<li>高延迟</li>
<li>错误返回</li>
</ul>
<p>提升系统鲁棒性</p>
<h3 id="4-用于开发早期阶段">4. 用于开发早期阶段</h3>
<p>在以下阶段优先使用模拟器：</p>
<ul>
<li>架构设计</li>
<li>API 设计</li>
<li>路由策略验证</li>
</ul>
<p>等到后期再接入真实模型。</p>
<h2 id="总结">总结</h2>
<p><strong>llm-d-inference-sim</strong> 解决的是一个被低估但关键的问题：</p>

<blockquote>
  <p>在没有模型的情况下，如何推进大模型工程化？</p>

</blockquote>
<p>它的核心价值不在于“模拟模型”，而在于：</p>
<ul>
<li>降低开发门槛</li>
<li>提升验证效率</li>
<li>解耦算力与工程</li>
</ul>
<p>对于正在构建 AI 基础设施（AI Gateway / 推理平台 / 多模型调度系统）的团队来说，这是一个非常实用的工具。</p>
]]></content:encoded></item><item><title>让模型自己选模型：Embedding 驱动的 LLM 智能路由机制</title><link>https://wilsonwu.me/zh/blog/2026/llm-smart-router/</link><pubDate>Tue, 26 May 2026 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2026/llm-smart-router/</guid><description>&lt;p&gt;在多模型并存的 AI 架构中（如 GPT-4 / GPT-4o / 轻量模型 / 垂直模型等），一个核心问题是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如何在不显式指定模型 ID 的情况下，让系统自动选择最合适的模型？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description><content:encoded><![CDATA[<p>在多模型并存的 AI 架构中（如 GPT-4 / GPT-4o / 轻量模型 / 垂直模型等），一个核心问题是：</p>

<blockquote>
  <p><strong>如何在不显式指定模型 ID 的情况下，让系统自动选择最合适的模型？</strong></p>

</blockquote>
<p>本文介绍一种工程可落地的方案：</p>

<blockquote>
  <p><strong>通过 Embedding 模型计算用户意图 → 在网关层进行语义匹配 → 动态路由到最合适的上游模型服务</strong></p>

</blockquote>
<p>我们将基于：</p>
<ul>
<li>Embedding 模型：Azure OpenAI <code>text-embedding-3-small</code></li>
<li>网关：Envoy</li>
<li>核心机制：向量相似度打分 + 策略路由</li>
</ul>
<h2 id="问题背景与动机">问题背景与动机</h2>
<p>在真实生产环境中，大模型调用通常面临几个挑战：</p>
<h3 id="1-成本与性能权衡">1. 成本与性能权衡</h3>
<table>
	<thead>
			<tr>
					<th>模型类型</th>
					<th>优点</th>
					<th>缺点</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>大模型（如 GPT-4）</td>
					<td>能力强</td>
					<td>成本高 / 延迟高</td>
			</tr>
			<tr>
					<td>小模型（如 GPT-4o-mini）</td>
					<td>快 / 便宜</td>
					<td>能力有限</td>
			</tr>
			<tr>
					<td>专用模型</td>
					<td>精准</td>
					<td>泛化弱</td>
			</tr>
	</tbody>
</table>
<p>不同请求应该走不同模型，而不是“一刀切”。</p>
<h3 id="2-传统做法的局限">2. 传统做法的局限</h3>
<p>常见策略：</p>
<ul>
<li>手动指定 model_id ❌（不智能）</li>
<li>基于规则（if/else）❌（不可扩展）</li>
<li>基于关键词匹配 ❌（语义不鲁棒）</li>
</ul>
<h3 id="3-理想目标">3. 理想目标</h3>
<p>我们希望：</p>

<blockquote>
  <p>用户只输入 prompt，系统自动理解“意图”，并选择最优模型</p>

</blockquote>
<p>这正是 Embedding 可以发挥作用的地方。</p>
<h2 id="核心原理embedding-驱动的语义路由">核心原理：Embedding 驱动的语义路由</h2>
<h3 id="1-embedding-本质">1. Embedding 本质</h3>
<p>Embedding 模型（如 <code>text-embedding-3-small</code>）可以将文本映射为高维向量：</p>
<pre tabindex="0"><code>&#34;写一段 Python 排序代码&#34;
→ [0.012, -0.98, ..., 0.33]  (1536维向量)
</code></pre><p>语义相似的文本，其向量距离更近。</p>
<h3 id="2-路由思路">2. 路由思路</h3>
<p>核心逻辑可以抽象为：</p>
<pre tabindex="0"><code>用户请求 → Embedding → 与“意图模板库”计算相似度 → 选择最佳匹配 → 路由模型
</code></pre><h3 id="3-意图模板intent-prototypes">3. 意图模板（Intent Prototypes）</h3>
<p>我们预先定义一组“意图向量”：</p>
<table>
	<thead>
			<tr>
					<th>意图类型</th>
					<th>示例文本</th>
					<th>目标模型</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>代码生成</td>
					<td>&ldquo;write code&rdquo;, &ldquo;generate function&rdquo;</td>
					<td>GPT-4</td>
			</tr>
			<tr>
					<td>简单问答</td>
					<td>&ldquo;what is&rdquo;, &ldquo;define&rdquo;</td>
					<td>GPT-4o-mini</td>
			</tr>
			<tr>
					<td>翻译</td>
					<td>&ldquo;translate this&rdquo;</td>
					<td>轻量模型</td>
			</tr>
			<tr>
					<td>长文生成</td>
					<td>&ldquo;write blog&rdquo;</td>
					<td>GPT-4</td>
			</tr>
	</tbody>
</table>
<p>这些文本会提前计算 embedding 并缓存。</p>
<h3 id="4-相似度计算">4. 相似度计算</h3>
<p>常用方法：</p>
<ul>
<li>Cosine Similarity（推荐）</li>
</ul>
<pre tabindex="0"><code>similarity = cosine(query_embedding, intent_embedding)
</code></pre><p>选择最高分：</p>
<pre tabindex="0"><code>argmax(similarity_i)
</code></pre><h2 id="系统架构设计">系统架构设计</h2>
<p>整体架构如下：</p>
<pre tabindex="0"><code>                ┌──────────────┐
                │   Client     │
                └──────┬───────┘
                       │
               ┌───────▼────────┐
               │    Envoy 网关   │
               │   (智能路由层)   │
               └───────┬────────┘
                       │
        ┌──────────────┼──────────────┐
        │              │              │
 ┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼─────┐
 │ GPT-4      │ │ GPT-4o-mini│ │ 专用模型    │
 └────────────┘ └────────────┘ └────────────┘
</code></pre><h2 id="实现逻辑拆解">实现逻辑拆解</h2>
<h3 id="1-请求流程">1. 请求流程</h3>
<h4 id="step-1获取用户请求">Step 1：获取用户请求</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;input&#34;</span>: <span style="color:#e6db74">&#34;帮我写一段 Go 的 HTTP server 示例&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="step-2调用-embedding-api">Step 2：调用 Embedding API</h4>
<p>使用 Azure OpenAI：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">POST https://{endpoint}/openai/deployments/text-embedding-3-small/embeddings?api-version=2024-02-15-preview
</span></span></span></code></pre></div><p>请求体：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;input&#34;</span>: <span style="color:#e6db74">&#34;帮我写一段 Go 的 HTTP server 示例&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="step-3计算相似度">Step 3：计算相似度</h4>
<p>伪代码：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">route</span>(query_embedding):
</span></span><span style="display:flex;"><span>    best_score <span style="color:#f92672">=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    best_model <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> intent <span style="color:#f92672">in</span> intents:
</span></span><span style="display:flex;"><span>        score <span style="color:#f92672">=</span> cosine_similarity(query_embedding, intent<span style="color:#f92672">.</span>embedding)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> score <span style="color:#f92672">&gt;</span> best_score:
</span></span><span style="display:flex;"><span>            best_score <span style="color:#f92672">=</span> score
</span></span><span style="display:flex;"><span>            best_model <span style="color:#f92672">=</span> intent<span style="color:#f92672">.</span>model
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> best_model
</span></span></code></pre></div><h4 id="step-4envoy-动态路由">Step 4：Envoy 动态路由</h4>
<p>将结果写入 Header：</p>
<pre tabindex="0"><code>x-llm-route: gpt-4
</code></pre><p>Envoy 根据 Header 路由：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">routes</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">prefix</span>: <span style="color:#e6db74">&#34;/v1/chat&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">x-llm-route</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exact_match</span>: <span style="color:#ae81ff">gpt-4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">route</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cluster</span>: <span style="color:#ae81ff">gpt4_service</span>
</span></span></code></pre></div><h2 id="envoy-网关实现方式">Envoy 网关实现方式</h2>
<p>Envoy 本身不直接支持向量计算，因此需要扩展：</p>
<h3 id="方案-1external-processing推荐">方案 1：External Processing（推荐）</h3>
<p>Envoy 调用外部服务（ext_proc）：</p>
<pre tabindex="0"><code>Envoy → ext_proc 服务 → 返回路由决策 → Envoy 转发
</code></pre><p>ext_proc 负责：</p>
<ul>
<li>调用 embedding</li>
<li>计算相似度</li>
<li>返回 header</li>
</ul>
<h3 id="方案-2wasm-插件">方案 2：WASM 插件</h3>
<p>在 Envoy 内嵌逻辑：</p>
<ul>
<li>优点：低延迟</li>
<li>缺点：开发复杂（Rust/CPP）</li>
</ul>
<h3 id="方案-3sidecar-router简单">方案 3：Sidecar Router（简单）</h3>
<pre tabindex="0"><code>Client → Router Service → Envoy → Model
</code></pre><p>Router 服务负责所有智能逻辑。</p>
<h2 id="优化策略">优化策略</h2>
<h3 id="1-向量缓存">1. 向量缓存</h3>
<p>避免重复调用 embedding：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>cache[input_hash] <span style="color:#f92672">=</span> embedding
</span></span></code></pre></div><h3 id="2-意图分层">2. 意图分层</h3>
<p>避免单层分类过粗：</p>
<pre tabindex="0"><code>一级：代码 / 问答 / 写作
二级：语言 / 长度 / 难度
</code></pre><h3 id="3-阈值控制">3. 阈值控制</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> best_score <span style="color:#f92672">&lt;</span> threshold:
</span></span><span style="display:flex;"><span>    fallback_model <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;gpt-4&#34;</span>
</span></span></code></pre></div><p>防止误判。</p>
<h3 id="4-多模型融合ensemble">4. 多模型融合（Ensemble）</h3>
<p>可扩展为：</p>
<ul>
<li>Top-K 模型候选</li>
<li>A/B 测试</li>
<li>加权路由</li>
</ul>
<h3 id="5-成本感知路由">5. 成本感知路由</h3>
<p>结合：</p>
<ul>
<li>token 预算</li>
<li>用户等级</li>
<li>SLA</li>
</ul>
<p>做策略增强：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> user <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;free&#34;</span>:
</span></span><span style="display:flex;"><span>    prefer_small_model()
</span></span></code></pre></div><h2 id="优势分析">优势分析</h2>
<h3 id="相比规则路由">相比规则路由</h3>
<table>
	<thead>
			<tr>
					<th>维度</th>
					<th>Embedding 路由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>语义理解</td>
					<td>强</td>
			</tr>
			<tr>
					<td>可扩展性</td>
					<td>高</td>
			</tr>
			<tr>
					<td>维护成本</td>
					<td>低</td>
			</tr>
	</tbody>
</table>
<h3 id="相比手动指定模型">相比手动指定模型</h3>
<ul>
<li>用户体验更好（无需关心模型）</li>
<li>自动最优成本</li>
<li>支持动态策略</li>
</ul>
<h2 id="落地建议">落地建议</h2>
<h3 id="1-初始阶段">1. 初始阶段</h3>
<ul>
<li>5~10 个意图模板即可</li>
<li>手动标注 + 调优</li>
</ul>
<h3 id="2-数据驱动优化">2. 数据驱动优化</h3>
<p>记录：</p>
<ul>
<li>用户请求</li>
<li>路由结果</li>
<li>用户反馈</li>
</ul>
<p>持续优化 intent embedding。</p>
<h3 id="3-与-llm-router-结合">3. 与 LLM Router 结合</h3>
<p>未来可以升级为：</p>

<blockquote>
  <p>用小模型 + embedding 混合决策（Hybrid Router）</p>

</blockquote>
<h2 id="总结">总结</h2>
<p>本文方案的本质是：</p>

<blockquote>
  <p><strong>用 Embedding 替代规则，用向量相似度驱动模型选择</strong></p>

</blockquote>
<p>关键收益：</p>
<ul>
<li>降低成本（智能选择轻量模型）</li>
<li>提升效果（复杂任务走强模型）</li>
<li>架构解耦（路由逻辑与模型解耦）</li>
</ul>
<p>在多模型时代，这种“语义驱动的网关”会成为标准架构组件。</p>
]]></content:encoded></item><item><title>微软 TRELLIS 大语言模型：面向生产级 3D 资产生成的大模型及基于 Azure 部署使用指南</title><link>https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/</link><pubDate>Mon, 19 Jan 2026 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/</guid><description>&lt;p&gt;&lt;img alt="Trellis-3D" loading="lazy" src="https://wilsonwu.me/zh/blog/2026/llm-microsoft-trellis-3d/1-trellis-3d.png"&gt;&lt;/p&gt;
&lt;p&gt;2025 年末，微软研究院发布了一个面向 3D 内容创作的开源大模型项目 &lt;strong&gt;TRELLIS&lt;/strong&gt;，并伴随学术论文&lt;a href="https://arxiv.org/abs/2412.01506"&gt;《Structured 3D Latents for Scalable and Versatile 3D Generation》&lt;/a&gt;。该项目通过统一的结构化潜在空间与先进的流模型技术，显著提升了文本/图像到 3D 资产生成的质量与灵活性，同时拓展了 3D 模型的多格式输出与编辑能力，成为当前 3D AI 模型生态中的重要技术之一。&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="Trellis-3D" loading="lazy" src="/zh/blog/2026/llm-microsoft-trellis-3d/1-trellis-3d.png"></p>
<p>2025 年末，微软研究院发布了一个面向 3D 内容创作的开源大模型项目 <strong>TRELLIS</strong>，并伴随学术论文<a href="https://arxiv.org/abs/2412.01506">《Structured 3D Latents for Scalable and Versatile 3D Generation》</a>。该项目通过统一的结构化潜在空间与先进的流模型技术，显著提升了文本/图像到 3D 资产生成的质量与灵活性，同时拓展了 3D 模型的多格式输出与编辑能力，成为当前 3D AI 模型生态中的重要技术之一。</p>
<p>官方 Github 仓库地址：<a href="https://github.com/microsoft/TRELLIS">https://github.com/microsoft/TRELLIS</a></p>
<h2 id="trellis-是什么核心概念与架构">TRELLIS 是什么？——核心概念与架构</h2>
<p>TRELLIS 是微软构建的<strong>大型 3D 资产生成模型</strong>，支持输入文本提示或图像，并输出高质量的三维模型资产。其技术创新点主要包括：</p>
<ul>
<li><strong>统一结构化潜在表示（SLAT）</strong>：将三维信息编码为一种可扩展的结构化潜在空间表示，使模型能够以统一方式理解和生成不同表现形式的 3D 结果。</li>
<li><strong>整流流（Rectified Flow Transformers）</strong>：专为 SLAT 设计的生成骨干网络，通过适应 sparse 表示提升生成效率与质量。</li>
<li><strong>大规模训练与预训练模型</strong>：模型规模最高达<strong>约 20 亿参数</strong>，并在包含 <strong>50 万多样化三维资产的数据集</strong>上训练，具备强泛化能力。</li>
</ul>
<p>TRELLIS 不仅能生成三维几何，还能捕获复杂纹理与外观信息，使得生成的资产更贴近真实世界中可用的三维内容。</p>
<h2 id="主要功能特性">主要功能特性</h2>
<p>以下是 TRELLIS 的核心功能模块及技术亮点：</p>
<h3 id="多模态输入能力">多模态输入能力</h3>
<p>TRELLIS 支持以下输入条件：</p>
<ul>
<li><strong>文本提示（Text-to-3D）</strong>：通过自然语言描述生成三维资产（提示须清晰准确）。</li>
<li><strong>图像条件（Image-to-3D）</strong>：根据一张或多张图片生成对应三维模型。</li>
</ul>
<p>这种多模态输入支持，使 TRELLIS 适用于从概念设计到真实原型的全流程创作。</p>
<p><img alt="3D" loading="lazy" src="/zh/blog/2026/llm-microsoft-trellis-3d/2-to-3d.png"></p>
<h3 id="灵活的输出格式">灵活的输出格式</h3>
<p>根据下游需求，TRELLIS 输出包括：</p>
<ul>
<li><strong>辐射场（NeRF/Radiance Fields）</strong>：适合渲染与展示</li>
<li><strong>3D 高斯表示</strong>：一种可渲染密度表示</li>
<li><strong>传统网格 Mesh</strong>：可导出为标准 3D 文件格式（例如 GLB/OBJ）用于游戏、AR/VR 等应用</li>
</ul>
<p>这种格式灵活性是模型在实际生产环境中落地的关键能力。</p>
<h3 id="本地三维编辑与变体生成">本地三维编辑与变体生成</h3>
<p>与早期单一生成模型不同，TRELLIS 能：</p>
<ul>
<li><strong>局部编辑</strong>：在已有模型上进行细部修改</li>
<li><strong>生成变体</strong>：在保持基本结构不变的前提下，创建风格、细节不同的多版本输出</li>
</ul>
<p>这种能力特别适合迭代式 3D 内容设计。</p>
<h3 id="支持大规模数据集训练与可扩展性">支持大规模数据集训练与可扩展性</h3>
<p>TRELLIS 在包含多种来源（如 Objaverse、ABO 等）的大规模 3D 数据集上进行训练，使其具备：</p>
<ul>
<li><strong>强泛化能力</strong></li>
<li><strong>更丰富的几何与纹理表现</strong></li>
<li><strong>多任务适配能力</strong></li>
</ul>
<p>这样的数据基础是其较竞品更具优势的根本原因之一。</p>
<h2 id="与其他竞品对比优势与提升点">与其他竞品对比：优势与提升点</h2>
<p>目前市场上其他比较知名的 3D 生成模型（例如 <strong>OpenAI Shap-E</strong>、或研究型模型如 MeshGen 等）多存在以下局限：</p>
<ul>
<li>有的只能输出单一 3D 表示（如内隐函数或点云）</li>
<li>多数不支持本地编辑与多种格式输出</li>
<li>在复杂纹理、细节表现上效果有限</li>
</ul>
<p>相比之下：</p>
<h3 id="trellis-的主要优势包括">TRELLIS 的主要优势包括</h3>
<ol>
<li><strong>结构化潜在表示（SLAT）框架</strong>：支持统一的生成与解码机制，同时兼顾几何与纹理细节。</li>
<li><strong>多格式生成输出</strong>：与只生成单种数据结构的竞品相比更通用。</li>
<li><strong>本地编辑与变体生成</strong>：提升了设计迭代效率。</li>
<li><strong>大规模预训练与更丰富数据集支撑</strong>：提升了模型对稀有或复杂对象的生成能力。</li>
</ol>
<p>总的来说，TRELLIS 更偏向工业级 3D 生成工具，而不只是科研展示或单一生成模型。</p>
<h2 id="部署和使用">部署和使用</h2>
<p>目前，微软尚未在 <strong>Azure AI Foundry / Microsoft Foundry</strong> 中以 Serverless 或托管模型的形式直接提供 TRELLIS。因此，在 Azure 云环境中使用 TRELLIS 的<strong>最可行、也是最稳定的方式</strong>，是基于 <strong>Azure GPU 虚拟机（VM）</strong>进行自托管部署。</p>
<p>这种方式具备以下优势：</p>
<ul>
<li>完全控制模型版本与推理流程</li>
<li>支持大显存 GPU，适合 3D 模型生成场景</li>
<li>可与企业现有 Azure 网络、安全、存储体系无缝集成</li>
</ul>
<h3 id="1-azure-gpu-vm-规格选型建议">1. Azure GPU VM 规格选型建议</h3>
<p>TRELLIS 属于<strong>显存与算力敏感型 3D 生成模型</strong>，对 GPU 资源要求较高，推荐如下规格：</p>
<h4 id="推荐-vm-系列">推荐 VM 系列</h4>
<ul>
<li><strong>Standard_NC A100 v4</strong>（A100 80GB，首选）</li>
<li><strong>Standard_NC A100 v5</strong>（A100 80GB，新一代）</li>
<li><strong>Standard_ND H100 v5</strong>（H100，若需要更高吞吐）</li>
</ul>

<blockquote>
  <p>实践建议：</p>
<ul>
<li><strong>推理 / Demo</strong>：单卡 A100 80GB 即可</li>
<li><strong>高分辨率 / 批量生成 / 编辑任务</strong>：建议单机多卡或更高规格</li>
</ul>

</blockquote>
<h3 id="2-操作系统与基础环境">2. 操作系统与基础环境</h3>
<p>推荐环境组合如下：</p>
<ul>
<li>OS：Ubuntu 20.04 / 22.04 LTS</li>
<li>CUDA：与 VM 驱动匹配的官方版本</li>
<li>Python：3.8 或以上</li>
<li>PyTorch：官方 CUDA 版本</li>
</ul>
<p>Azure Marketplace 中可直接选择 <strong>CUDA + NVIDIA Driver 预装镜像</strong>，可显著降低环境准备成本。</p>
<h3 id="3-trellis-安装与依赖配置">3. TRELLIS 安装与依赖配置</h3>
<h4 id="拉取代码">拉取代码</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone --recurse-submodules https://github.com/microsoft/TRELLIS.git
</span></span><span style="display:flex;"><span>cd TRELLIS
</span></span></code></pre></div><h4 id="创建独立-python-环境并安装依赖">创建独立 Python 环境并安装依赖</h4>
<p>官方已提供自动化脚本：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./setup.sh --new-env --basic
</span></span></code></pre></div><p>如需支持训练、编辑或高级功能，可根据官方说明启用对应参数。</p>
<h3 id="4-基于-azure-vm-的推理运行方式">4. 基于 Azure VM 的推理运行方式</h3>
<h4 id="方式一命令行推理适合批量任务">方式一：命令行推理（适合批量任务）</h4>
<p>文本或图像驱动生成 3D 模型：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python example_text.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --prompt <span style="color:#e6db74">&#34;a futuristic electric motorcycle&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --output_dir outputs/
</span></span></code></pre></div><p>或多图像条件输入：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python example_multi_image.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --input_images images/ <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --output_dir outputs/
</span></span></code></pre></div><p>生成结果可直接导出为 Mesh / Radiance Field 等格式，用于后续渲染或工具链集成。</p>
<h4 id="方式二gradio-web-ui推荐用于演示与验证">方式二：Gradio Web UI（推荐用于演示与验证）</h4>
<p>TRELLIS 官方提供基于 Gradio 的 Web Demo，适合：</p>
<ul>
<li>团队内部体验</li>
<li>产品验证</li>
<li>算法效果对比</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python app.py
</span></span></code></pre></div><p>然后通过 Azure VM 公网 IP 或内网负载均衡访问 Web UI。</p>
<h3 id="5-azure-上的工程化部署建议">5. Azure 上的工程化部署建议</h3>
<p>为了更好地在 Azure 上长期运行 TRELLIS，建议采用以下架构实践：</p>
<h4 id="存储解耦">存储解耦</h4>
<ul>
<li>模型权重：Azure Blob Storage</li>
<li>生成结果：Blob / Azure Files</li>
<li>日志与元数据：Azure Monitor / Log Analytics</li>
</ul>
<h4 id="服务化封装">服务化封装</h4>
<ul>
<li>使用 <strong>FastAPI / Flask</strong> 将 TRELLIS 推理封装为 HTTP API</li>
<li>GPU VM 作为推理节点</li>
<li>前端或工作流通过 REST 调用</li>
</ul>
<h4 id="安全与网络">安全与网络</h4>
<ul>
<li>VM 置于 VNet 内</li>
<li>Web UI / API 通过 Application Gateway 或内部访问</li>
<li>支持私有部署，满足企业安全要求</li>
</ul>
<h2 id="总结与展望">总结与展望</h2>
<p>微软 TRELLIS 代表了当前 3D AI 生成技术的重要进步，它在生成质量、格式多样性与本地编辑能力方面具备显著优势。对于希望自动化 3D 内容创建的开发者与设计者而言，它是一个有实力的开源方案。部署方面，虽然目前官方尚未将其作为 Foundry 的 serverless 模型，但可通过 Azure 的常规模型部署管道与 GPU 计算能力在云端高效运行 TRELLIS 推理与自定义训练。</p>
]]></content:encoded></item><item><title>玩转大语言模型：为初学者扫清大语言模型（LLM）架构盲区</title><link>https://wilsonwu.me/zh/blog/2025/llm-architecture/</link><pubDate>Wed, 10 Dec 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/llm-architecture/</guid><description>&lt;p&gt;过去五年，大语言模型（Large Language Models, LLMs）的发展几乎完全重塑了人工智能的技术版图。从 GPT 到 LLaMA，从 Transformer 到 Mixture-of-Experts（MoE），从单体模型到大规模分布式参数服务器体系，架构演进直接推动了能力跃迁。&lt;/p&gt;</description><content:encoded><![CDATA[<p>过去五年，大语言模型（Large Language Models, LLMs）的发展几乎完全重塑了人工智能的技术版图。从 GPT 到 LLaMA，从 Transformer 到 Mixture-of-Experts（MoE），从单体模型到大规模分布式参数服务器体系，架构演进直接推动了能力跃迁。</p>
<p>本文将从架构层面系统梳理 LLM 的主流技术路径，并从应用视角分析其优劣及适配场景，为研发与业务团队提供技术选型参考。同时也希望可以为初学者们打开迈入大模型世界的一条门缝！</p>
<h2 id="大语言模型的主流架构体系">大语言模型的主流架构体系</h2>
<p><img alt="LLM" loading="lazy" src="/zh/blog/2025/llm-architecture/1-llm.png"></p>
<p>目前的 LLM 架构，大概可以分为这么几类：</p>
<ol>
<li><strong>传统 Transformer（Dense Transformer）</strong></li>
<li><strong>Mixture-of-Experts（MoE）架构</strong></li>
<li><strong>多模态扩展架构（Vision-Language / Audio-Language）</strong></li>
<li><strong>检索增强生成（RAG）与混合推理架构</strong></li>
<li><strong>基于代理（Agentic）架构的系统级 LLM</strong></li>
<li><strong>超大规模分布式训练架构（例如 Parameter Server / Fully Sharded）</strong></li>
</ol>
<p>下面逐一展开。</p>
<h2 id="dense-transformer">传统 Transformer：主流 LLM 的基础架构</h2>
<p>Dense Transformer 是绝大多数 LLM 的基座，包括 GPT 系列、LLaMA、Mistral、Qwen 等。</p>
<h3 id="dense-transformer-architecture">架构</h3>
<p><img alt="Dense Transformer" loading="lazy" src="/zh/blog/2025/llm-architecture/2-dense-transformer.png"></p>
<p>简单来说，就是“大力出奇迹”：</p>
<ul>
<li>所有 token 通过全连接注意力进行计算；</li>
<li>全部参数在每次前向推理中都会被激活；</li>
<li>结构相对规则，训练稳定；</li>
<li>模型规模靠堆叠层数与扩大 hidden size 线性提升。</li>
</ul>
<h3 id="dense-transformer-advantages">优势</h3>
<ul>
<li>推理路径稳定、可预测；</li>
<li>训练过程成熟，生态支持丰富；</li>
<li>对所有任务通用，不需要额外路由机制。</li>
</ul>
<h3 id="dense-transformer-limitations">局限</h3>
<ul>
<li>参数规模大时推理成本高（全部参数激活）；</li>
<li>扩展模型能力的成本几乎与参数规模线性相关。</li>
</ul>
<h3 id="dense-transformer-scenarios">应用场景</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>适用性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>通用对话</td>
					<td>高</td>
			</tr>
			<tr>
					<td>编程、数学推理</td>
					<td>高</td>
			</tr>
			<tr>
					<td>对实时需求强的应用（低延迟）</td>
					<td>较低</td>
			</tr>
			<tr>
					<td>资源受限设备（边缘推理）</td>
					<td>一般，可用量化缓解</td>
			</tr>
	</tbody>
</table>
<p>Dense 模型依然是绝大多数企业首次采用 LLM 时的首选。</p>
<h2 id="moe">Mixture-of-Experts（MoE）：高效扩展能力的关键技术</h2>
<p>MoE 是当前最重要的新架构之一，代表模型包括：</p>
<ul>
<li>Google Switch Transformer</li>
<li>DeepSeek-V2/V3</li>
<li>Mixtral 系列</li>
<li>GPT-4 技术论文暗示其有 MoE 结构成分</li>
</ul>
<h3 id="moe-architecture">架构</h3>
<p>MoE 不再让所有参数参与每次计算，而是通过<strong>路由（Routing）</strong>机制让每个 token 只激活若干专家层（Expert）。</p>
<p><img alt="Token Choice Routing" loading="lazy" src="/zh/blog/2025/llm-architecture/3-moe-token-choice-routing.png"></p>
<p><img alt="Expert Choice Routing" loading="lazy" src="/zh/blog/2025/llm-architecture/4-moe-expert-choice-routing.png"></p>
<p>举个例子，一个 300B 参数的庞然大物，实际每次推理可能只动用了 30B 的参数。</p>
<h3 id="moe-advantages">优势</h3>
<ul>
<li><strong>训练扩展性极强</strong>：参数规模可以以更低成本提升到数千亿甚至上万亿。</li>
<li><strong>推理成本显著降低</strong>：激活专家减少推理 FLOPs。</li>
<li><strong>擅长复杂推理与多任务能力</strong>。</li>
</ul>
<h3 id="moe-limitations">局限</h3>
<ul>
<li>路由训练复杂，稳定性需求更高；</li>
<li>推理并行规模更大，对系统工程要求高；</li>
<li>不适合极端低延迟场景，因为专家调度带来额外开销。</li>
</ul>
<h3 id="moe-scenarios">应用场景</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>适用性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>多领域任务（代码/数学/语言混合）</td>
					<td>极高</td>
			</tr>
			<tr>
					<td>超大规模模型（百亿+）</td>
					<td>极高</td>
			</tr>
			<tr>
					<td>边缘设备、小型模型</td>
					<td>低</td>
			</tr>
			<tr>
					<td>多租户大规模在线推理</td>
					<td>依实现而定</td>
			</tr>
	</tbody>
</table>
<p>MoE 已成为大型商业 LLM 的主流趋势。</p>
<h2 id="multimodal">多模态 LLM 架构：模型能力向感知延伸</h2>
<p>代表模型：GPT-4o、Gemini、Qwen-VL、LLaVA、Yi-VL</p>
<h3 id="multimodal-architecture">架构</h3>
<p>多模态 LLM 通过在 Transformer 基座上引入额外编码器，将视觉、音频、视频等信号映射为 token 表示。</p>
<p><img alt="多模态 LLM 架构" loading="lazy" src="/zh/blog/2025/llm-architecture/5-multimodal.png"></p>
<p>典型结构通常包含：</p>
<ul>
<li>视觉编码器（Vision Encoder，比如 ViT）</li>
<li>投影层（Projection Layer）</li>
<li>统一的 Transformer</li>
<li>多模态注意力机制</li>
</ul>
<h3 id="multimodal-advantages">优势</h3>
<ul>
<li>输入能力扩展显著：图像、音频、视频、文档统合处理；</li>
<li>极适合作为通用 AI Agent 的感知模块。</li>
</ul>
<h3 id="multimodal-limitations">局限</h3>
<ul>
<li>成本比纯文本 LLM 更高；</li>
<li>多模态数据构建成本大；</li>
<li>输出的稳定性与高保真图像/视频理解仍有上限。</li>
</ul>
<h3 id="multimodal-scenarios">应用场景</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>适用性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>OCR / 文档理解</td>
					<td>极高</td>
			</tr>
			<tr>
					<td>多模态搜索</td>
					<td>高</td>
			</tr>
			<tr>
					<td>视频理解</td>
					<td>中（依赖模型）</td>
			</tr>
			<tr>
					<td>紧凑型设备部署</td>
					<td>低</td>
			</tr>
	</tbody>
</table>
<p>随着 GPT-4o 全模态输入的普及，多模态 LLM 已成为企业 AI 转型的必备能力层。</p>
<h2 id="hybridrag">RAG + LLM 混合架构：以知识增强为核心的系统形态</h2>
<p>RAG（Retrieval-Augmented Generation）通过将外部知识库融合到推理中解决 LLM 本身无法持久更新和存储大量事实的问题。</p>
<h3 id="hybridrag-architecture">架构</h3>
<p>由三个核心组件构成：</p>
<ul>
<li>向量数据库（Retriever）</li>
<li>文档预处理管线（Chunking, Indexing）</li>
<li>核心 LLM（Generator）</li>
</ul>
<p><img alt="混合架构" loading="lazy" src="/zh/blog/2025/llm-architecture/6-hybridrag.png"></p>
<h3 id="hybridrag-advantages">优势</h3>
<ul>
<li>模型无需重新训练即可持续更新知识；</li>
<li>显著增强长尾知识与事实性能力；</li>
<li>数据可控，便于企业级治理。</li>
</ul>
<h3 id="hybridrag-limitations">局限</h3>
<ul>
<li>依赖检索质量；</li>
<li>对 prompt 工程与 pipeline 工程化要求高；</li>
<li>多跳检索在 latency 上存在开销。</li>
</ul>
<h3 id="hybridrag-scenarios">应用场景</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>适用性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>企业知识库问答</td>
					<td>极高</td>
			</tr>
			<tr>
					<td>法律、医疗等高准确性应用</td>
					<td>高</td>
			</tr>
			<tr>
					<td>实时 LLM 应用（对时延敏感）</td>
					<td>较低</td>
			</tr>
	</tbody>
</table>
<p>RAG 是企业级 LLM 落地中最常见的架构。</p>
<h2 id="agentic">基于代理的架构：基于智能体的系统级 LLM 架构</h2>
<p>代表：AutoGPT、GPT-o1、GPT-o3、DeepSeek-R1、自主推理 Agent Framework</p>
<p>随着模型具备规划（Planning）、执行（Acting）和反思（Reflection）能力，Agent 架构正在成为应用层主流。</p>
<h3 id="agentic-architecture">架构</h3>
<ul>
<li>多工具整合（Tool Use）</li>
<li>工作流管理</li>
<li>多轮子任务规划</li>
<li>Memory 与外部状态管理</li>
</ul>
<p><img alt="智能体架构" loading="lazy" src="/zh/blog/2025/llm-architecture/7-agentic.png"></p>
<h3 id="agentic-advantages">优势</h3>
<ul>
<li>可执行复杂任务链；</li>
<li>强化模型可控性；</li>
<li>工具能力扩展模型本身的局限。</li>
</ul>
<h3 id="agentic-limitations">局限</h3>
<ul>
<li>系统复杂度显著增加；</li>
<li>可靠性、可追踪性和可验证性要求高；</li>
<li>调度能力直接影响实际效果。</li>
</ul>
<h3 id="agentic-scenarios">应用场景</h3>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>适用性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>自动化运营</td>
					<td>极高</td>
			</tr>
			<tr>
					<td>数据分析助手</td>
					<td>高</td>
			</tr>
			<tr>
					<td>多步骤计算任务</td>
					<td>高</td>
			</tr>
			<tr>
					<td>回答型问答</td>
					<td>一般</td>
			</tr>
			<tr>
					<td>超低延迟场景</td>
					<td>不适合</td>
			</tr>
	</tbody>
</table>
<p>Agent 架构代表了 LLM 应用的未来方向。</p>
<h2 id="distributed-training">分布式训练架构：支撑万亿参数模型的底层工程</h2>
<p><img alt="分布式训练" loading="lazy" src="/zh/blog/2025/llm-architecture/8-distributed-training.png"></p>
<p>这类方案通常没有特定的架构模型，依照不同的需求，技术厂商会演进出各类方案，比如：</p>
<ul>
<li>ZeRO / FSDP（Fully Sharded Data Parallel）</li>
<li>Megatron-LM（Tensor / Pipeline 并行）</li>
<li>DeepSpeed</li>
<li>Colossal-AI</li>
<li>Parameter Server（SageMaker、Ray、DeepSeek 自研）</li>
</ul>
<h3 id="distributed-training">优势</h3>
<ul>
<li>高效训练超大模型；</li>
<li>利用 GPU 集群最大化吞吐；</li>
<li>优化显存占用。</li>
</ul>
<h3 id="distributed-training">局限</h3>
<ul>
<li>工程复杂度极高；</li>
<li>部署成本昂贵；</li>
<li>对 GPU 集群架构高度依赖。</li>
</ul>
<h3 id="distributed-training">应用场景</h3>
<p>主要适用于自研大模型或千卡级 GPU 训练任务的企业或科研团队。</p>
<h2 id="各架构侧重点及优势对比">各架构侧重点及优势对比</h2>
<p>以下为一张综合对比表：</p>
<table>
	<thead>
			<tr>
					<th>架构类型</th>
					<th>优势</th>
					<th>劣势</th>
					<th>应用重点</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Dense Transformer</td>
					<td>稳定、成熟、推理链简单</td>
					<td>成本随规模线性提升</td>
					<td>通用模型、边缘部署</td>
			</tr>
			<tr>
					<td>MoE</td>
					<td>高效扩展、高性价比</td>
					<td>路由复杂、工程门槛高</td>
					<td>大规模模型、混合任务</td>
			</tr>
			<tr>
					<td>多模态 LLM</td>
					<td>输入能力强</td>
					<td>成本高、训练复杂</td>
					<td>文档/图像/视频/音频场景</td>
			</tr>
			<tr>
					<td>RAG</td>
					<td>强事实性、可更新</td>
					<td>检索依赖高</td>
					<td>企业知识库、法律/医疗</td>
			</tr>
			<tr>
					<td>Agent</td>
					<td>工具能力强、任务链执行</td>
					<td>系统复杂</td>
					<td>自动化任务、数据分析</td>
			</tr>
			<tr>
					<td>分布式架构</td>
					<td>支撑万亿模型</td>
					<td>成本与复杂度高</td>
					<td>训练基建</td>
			</tr>
	</tbody>
</table>
<h2 id="如何选择适合的-llm-架构">如何选择适合的 LLM 架构？</h2>
<p>基于上述介绍的 LLM 架构，我们初步了解了大语言模型本身的架构以及叠加一些应用场景的方案架构，而在 LLM 结合各个行业的真实应用场景中，架构远远不止这些，结合已有的计算机系统架构，LLM 的架构也在不断进化和丰富中，下面简单根据应用需求，推荐如下路径：</p>
<h3 id="目标是开发一个企业级问答系统">目标是开发一个企业级问答系统</h3>
<p>Dense Transformer + 强化 RAG：可靠、易控、低成本、可持续更新。</p>
<p><img alt="问答系统" loading="lazy" src="/zh/blog/2025/llm-architecture/9-qa-system.png"></p>
<h3 id="目标是打造一个全能基础大模型系统">目标是打造一个全能基础大模型系统</h3>
<p>MoE 是优选：性能与性价比兼得。</p>
<p><img alt="全能大模型系统" loading="lazy" src="/zh/blog/2025/llm-architecture/10-all-in-one-llm.png"></p>
<h3 id="目标是构建文档图像视频-ai-系统">目标是构建文档/图像/视频 AI 系统</h3>
<p>多模态 LLM：例如 GPT-4o、Qwen-VL、Gemini。</p>
<p><img alt="多模态架构" loading="lazy" src="/zh/blog/2025/llm-architecture/11-mm-arch.png"></p>
<h3 id="需要自动化任务执行">需要自动化任务执行</h3>
<p>基于代理的架构：模型本身不需要特别大，但需要良好的工具接口和规划能力。</p>
<p><img alt="自动化任务系统" loading="lazy" src="/zh/blog/2025/llm-architecture/12-llm-and-agent.png"></p>
<h3 id="需要训练和运行自研模型">需要训练和运行自研模型</h3>
<p>FSDP / Megatron / PS-based 分布式架构，这类需求其实可选范围非常广，由于是训推整体方案，只要沿用市面成型的一体方案即可，不需要花大量时间在训练和推理框架之间做兼容。</p>
<p><img alt="训推系统" loading="lazy" src="/zh/blog/2025/llm-architecture/13-training-inference.png"></p>
<h2 id="总结">总结</h2>
<p>大语言模型架构的演进已经进入百花齐放阶段。Dense 模型提供稳定基座，MoE 架构带来成本与能力的突破，多模态扩展了 AI 的感知边界，而 RAG 与 Agent 架构将 LLM 推向真正可控、可执行的智能体时代。</p>
<p>对企业而言，选对架构比盲目追求“更大模型”更重要。依据实际需求、工程能力和预算进行架构选择，既能优化效果，也能确保长期可维护性。</p>
]]></content:encoded></item><item><title>玩转大语言模型：深入理解 KV-Cache - 大模型推理的核心加速技术</title><link>https://wilsonwu.me/zh/blog/2025/kv-cache-in-llm/</link><pubDate>Tue, 18 Nov 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/kv-cache-in-llm/</guid><description>&lt;p&gt;随着大语言模型（LLM）规模不断增长，推理成本也随之飙升。为了让模型在响应用户请求时更快、更经济地运行，各类优化技术不断涌现。其中，&lt;strong&gt;KV-Cache（Key-Value Cache）&lt;/strong&gt;是目前最关键、影响最深远的推理加速机制之一，被所有主流的推理框架（如 vLLM、TensorRT-LLM、LLama.cpp、llm-d、OpenAI Triton Transformer Engine 等）广泛使用。&lt;/p&gt;</description><content:encoded><![CDATA[<p>随着大语言模型（LLM）规模不断增长，推理成本也随之飙升。为了让模型在响应用户请求时更快、更经济地运行，各类优化技术不断涌现。其中，<strong>KV-Cache（Key-Value Cache）</strong>是目前最关键、影响最深远的推理加速机制之一，被所有主流的推理框架（如 vLLM、TensorRT-LLM、LLama.cpp、llm-d、OpenAI Triton Transformer Engine 等）广泛使用。</p>
<p>这篇文章将全面介绍什么是 KV-Cache、它如何工作、为什么它能极大提升推理效率、它对行业带来了什么影响，以及在实际使用中的最佳实践。</p>
<h2 id="kv-cache-是什么">KV-Cache 是什么？</h2>
<p>首先，其工作原理架构图如下：</p>
<p><img alt="架构图" loading="lazy" src="/zh/blog/2025/kv-cache-in-llm/1-architecture.png"></p>
<p>理解起来非常简单，单其中细节结合复杂度计算还是略有抽象。</p>
<p>KV-Cache，全称 <strong>Key-Value Cache</strong>，是在 LLM 推理过程中对 Transformer 解码器中<strong>自注意力层（Self-Attention）</strong>的中间结果进行缓存的一种方法。</p>
<p>Transformer 的自回归生成方式决定了：
<strong>每生成一个新 token，就需要重新计算它与全部历史 token 的注意力关系</strong>。</p>
<p>如果每次都把过去所有 token 的 Key 和 Value 重新计算一遍，其计算量是：</p>

<blockquote>
  <p>推理时间复杂度：O(n²)
（n 为上下文长度）</p>

</blockquote>
<p>为了避免重复计算，KV-Cache 会在每步生成 token 时，将计算后的 <strong>Key（K）</strong> 和 <strong>Value（V）</strong> 保存下来，这样之后再生成下一步 token 时就能直接引用过去的 Key/Value，而无需重新计算历史部分。</p>
<p>这让时间复杂度大幅下降为：</p>

<blockquote>
  <p>推理时间复杂度：O(n)</p>

</blockquote>
<p>所以简单总结一下，KV-Cache 是在自回归解码中缓存历史 token 的 Key/Value，让后续生成直接复用过去的注意力结果，从而把时间复杂度从 O(n²) 降到 O(n)，节省的成本非常巨大。</p>
<h2 id="kv-cache-是如何工作的">KV-Cache 是如何工作的？</h2>
<p>以当前主流的解码流程为例，我们以是否使用 KV-Cache 来做一个简单的对比。</p>
<h3 id="如果没有-kv-cache">如果没有 KV-Cache</h3>
<p>每次生成新 token 时需要做：</p>
<ol>
<li>重新对全部历史序列做 embedding</li>
<li>通过所有 Transformer 层重新计算 K/V</li>
<li>根据新 token 与整个序列做自注意力</li>
<li>得到新 token 并输出</li>
</ol>
<p>具体流程图示如下：</p>
<p><img alt="不使用 KV-Cache" loading="lazy" src="/zh/blog/2025/kv-cache-in-llm/2-without-kv-cache.png"></p>
<h3 id="使用-kv-cache">使用 KV-Cache</h3>
<p>第一次推理（prefill 阶段）：</p>
<ol>
<li>对所有提示词（prompt tokens）进行计算</li>
<li>计算所有 K/V，并将它们保存在 KV 缓存中</li>
</ol>
<p>之后的每一步（decode 阶段）：</p>
<ol>
<li>只计算当前新 token 的 K/V</li>
<li>与缓存中的 K/V 做注意力，得到结果</li>
<li>写回缓存</li>
</ol>
<p>这样，每生成一个 token 的计算量从<strong>全序列规模 → 近似常数开销</strong>，越长的上下文收益越大。</p>
<p>其流程图示如下：</p>
<p><img alt="使用 KV-Cache" loading="lazy" src="/zh/blog/2025/kv-cache-in-llm/3-with-kv-cache.png"></p>
<h2 id="kv-cache-带来了什么影响">KV-Cache 带来了什么影响？</h2>
<h3 id="推理速度提升数十倍">推理速度提升数十倍</h3>
<p>对长上下文模型（8k / 32k / 128k tokens）尤其明显。</p>
<p>例如对 32k token 的上下文，如果没有 KV-Cache：</p>
<ul>
<li>每生成一个 token 需要遍历 32k 历史序列</li>
<li>整体是 <em>重复 32k 次的巨量工作</em></li>
</ul>
<p>有了 KV-Cache：</p>
<ul>
<li>历史部分只算一次</li>
<li>单步 decode 的开销变成固定量（几百维 KV 与注意力点积）</li>
</ul>
<p>通常加速比可达到：</p>

<blockquote>
  <p>10 倍至 100 倍不等
（取决于提示词长度、模型大小、硬件）</p>

</blockquote>
<h3 id="推理成本大幅降低">推理成本大幅降低</h3>
<p>大模型推理的成本主要由这几个部分构成：</p>
<ul>
<li><strong>算力</strong>（矩阵乘法）</li>
<li><strong>显存占用</strong></li>
<li><strong>带宽</strong></li>
</ul>
<p>KV-Cache 能大幅降低重复算力，因此成本成比例下降。</p>
<p>这就是为什么：</p>
<ul>
<li>OpenAI、Anthropic、Meta、Mistral</li>
<li>各大云推理服务</li>
<li>所有开源推理框架</li>
</ul>
<p>都把 KV-Cache 作为默认推理方式。</p>
<h3 id="但是显存占用会变高">但是显存占用会变高</h3>
<p>KV 是每一层每个 token 的结果，因此缓存本身需要<strong>巨大的显存</strong>。</p>
<p>例如：
Llama-3 70B 模型，64K context，对 1 token 的 KV 占用约 1.5KB。</p>
<p>那么 64K token 就是：<strong>约 96MB × 80 层 ≈ 7.6GB 的 KV-cache</strong></p>
<p>当你看到推理卡 80GB 显存不够时，很大概率是因为 KV 满了，而不是模型权重太大。</p>
<h3 id="影响了模型的架构设计与行业走向">影响了模型的架构设计与行业走向</h3>
<p>KV-Cache 的重要性高到：<strong>模型架构都围绕它做优化。</strong></p>
<p>包括：</p>
<ul>
<li>Mistral 的<strong>滑动窗口注意力（SWA）</strong></li>
<li>Meta 的 <strong>Grouped Query Attention（GQA）</strong></li>
<li>DeepSeek 的 <strong>Multi-head Latent Attention（MLA）</strong></li>
<li>Google 的 <strong>Multi-Query Attention（MQA）</strong></li>
</ul>
<p>这些技术主要目的之一就是：</p>

<blockquote>
  <p>降低 KV-Cache 的显存占用</p>

</blockquote>
<p>因为 Q 数少 → KV 也变少 → 显存压力更低。</p>
<p>所以 <strong>KV-Cache 不只是一个技术点，而是影响模型架构的重要因素</strong>。</p>
<h2 id="kv-cache-的性能关键因素">KV-Cache 的性能关键因素</h2>
<h3 id="不同-attention-架构带来的-kv-大小差异">不同 Attention 架构带来的 KV 大小差异</h3>
<table>
	<thead>
			<tr>
					<th>Attention 类型</th>
					<th>KV 大小</th>
					<th>优点</th>
					<th>缺点</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>MHA（多头注意力）</td>
					<td>最大</td>
					<td>兼容性最好</td>
					<td>显存占用高</td>
			</tr>
			<tr>
					<td>MQA（多查询注意力）</td>
					<td>小很多</td>
					<td>更高效</td>
					<td>灵活性略差</td>
			</tr>
			<tr>
					<td>GQA（分组注意力）</td>
					<td>居中</td>
					<td>性能好</td>
					<td>结构更复杂</td>
			</tr>
			<tr>
					<td>MLA</td>
					<td>更小</td>
					<td>更快</td>
					<td>模型支持有限</td>
			</tr>
	</tbody>
</table>
<h3 id="prefill-和-decode-性能壁垒不同">Prefill 和 Decode 性能壁垒不同</h3>
<ul>
<li><strong>Prefill 阶段</strong>：算 K/V 很耗算力，需要好 GPU。</li>
<li><strong>Decode 阶段</strong>：显存带宽优先，因为每步生成通常 batch 很小。</li>
</ul>
<p>这也影响推理框架的优化策略。</p>
<h3 id="kv-cache-的布局与管理">KV-Cache 的布局与管理</h3>
<p>例如：</p>
<ul>
<li><strong>PagedAttention</strong>（vLLM 的核心技术）</li>
<li><strong>Block-sparse KV 管理</strong>（TensorRT-LLM）</li>
<li><strong>动态缓存分配</strong>（llama.cpp）</li>
<li><strong>连续 vs 分块 KV 内存</strong></li>
</ul>
<p>优秀的分配策略可以极大减少 fragmentation 和显存浪费。</p>
<h2 id="kv-cache-的最佳实践">KV-Cache 的最佳实践</h2>
<h3 id="尽量减少上下文长度">尽量减少上下文长度</h3>
<p>上下文越长 → KV 越大 → 推理越慢、越贵。</p>
<p>如果业务允许：</p>
<ul>
<li>使用 <strong>RAG</strong> 把长文档拆分</li>
<li>使用<strong>压缩模型（compressor）</strong></li>
<li>使用<strong>短指令模板</strong></li>
<li>引导模型减少“废话”输出</li>
</ul>
<h3 id="选择合适的-attention-架构">选择合适的 Attention 架构</h3>
<p>如果你在自定义模型、微调或部署模型：</p>
<ul>
<li>需要高效推理 → 优选 <strong>MQA / GQA / MLA</strong></li>
<li>需要兼容许多任务 → MHA 仍然最稳定</li>
</ul>
<h3 id="使用专业推理框架">使用专业推理框架</h3>
<p>优先使用：</p>
<ul>
<li><strong>vLLM（商业与开源界第一推理框架）</strong></li>
<li><strong>TensorRT-LLM（NVIDIA 官方）</strong></li>
<li><strong>llm-d（中小型企业的极高性价比框架）</strong></li>
<li><strong>OpenAI Triton 引擎（内部使用）</strong></li>
</ul>
<p>它们都在 KV-Cache 管理上有深度优化。</p>
<h3 id="多卡推理时注意-kv-分布问题">多卡推理时注意 KV 分布问题</h3>
<p>KV 会引发：</p>
<ul>
<li><strong>多卡同步开销</strong></li>
<li><strong>all-reduce 带宽瓶颈</strong></li>
</ul>
<p>部分框架提供：</p>
<ul>
<li>KV sharding</li>
<li>pipeline parallelism</li>
<li>block-sparse KV sharing</li>
</ul>
<p>合理设置可显著提升吞吐量。</p>
<h3 id="批处理策略影响巨大">批处理策略影响巨大</h3>
<p>适配 KV-Cache 的批处理策略包括：</p>
<ul>
<li>continuous batching</li>
<li>block scheduling</li>
<li>prompt+decode 混合 batch</li>
</ul>
<p>正确 batching 通常带来 <strong>&gt;10× 吞吐提升</strong>。</p>
<h2 id="kv-cache-的未来展望">KV-Cache 的未来展望</h2>
<p>未来几年，模型推理的发展趋势中，KV-Cache 会继续是核心机制。</p>
<p>我们将看到：</p>
<ul>
<li>更轻量的 KV 结构（更少头、更小维度）</li>
<li>更智能的 KV 重用机制</li>
<li>KV 的跨请求共享（multi-query serving）</li>
<li>部分 KV 的 CPU/NVMe offload</li>
<li>Push-down Attention（降低 GPU 带宽要求）</li>
</ul>
<p>总之，KV-Cache 将继续驱动推理性能提升，是 LLM 推理领域最重要的基础设施之一。</p>
<h2 id="总结">总结</h2>
<p>KV-Cache 并不是一个“可选优化”，而是现代大语言模型推理的 <strong>核心基石</strong>。
它带来巨大的速度提升，但也带来显存压力，影响模型架构、推理框架设计乃至行业整个技术路线。</p>
<p>理解 KV-Cache，你就理解了：</p>
<ul>
<li>为什么推理这么贵</li>
<li>为什么上下文越长模型越慢</li>
<li>为什么新一代模型都在改注意力结构</li>
<li>为什么推理框架竞争如此激烈</li>
</ul>
]]></content:encoded></item><item><title>玩转大语言模型：轻松使用 Azure AI Foundry 提供的 Sora 2 生成视频</title><link>https://wilsonwu.me/zh/blog/2025/azure-ai-foundry-sora-2/</link><pubDate>Mon, 10 Nov 2025 00:00:00 -0500</pubDate><guid>https://wilsonwu.me/zh/blog/2025/azure-ai-foundry-sora-2/</guid><description>&lt;p&gt;随着 Azure AI Foundry 开放对 &lt;strong&gt;Sora 2（OpenAI 生成式视频模型）&lt;/strong&gt;的支持，开发者现在可以在企业级合规、可管控的环境中使用顶尖的视频生成能力。本教程将带你从零开始，通过 &lt;strong&gt;Playground&lt;/strong&gt; 和 &lt;strong&gt;Python SDK&lt;/strong&gt; 两种方式调用 Sora 2，完成「文本生成视频」的流程。&lt;/p&gt;</description><content:encoded><![CDATA[<p>随着 Azure AI Foundry 开放对 <strong>Sora 2（OpenAI 生成式视频模型）</strong>的支持，开发者现在可以在企业级合规、可管控的环境中使用顶尖的视频生成能力。本教程将带你从零开始，通过 <strong>Playground</strong> 和 <strong>Python SDK</strong> 两种方式调用 Sora 2，完成「文本生成视频」的流程。</p>
<h2 id="准备工作">准备工作</h2>
<p>在开始之前，你需要：</p>
<h3 id="获取-azure-订阅">获取 Azure 订阅</h3>
<p>拥有一个 <strong>Azure 订阅</strong>，如果您不清楚如何获取 Azure 订阅，可以参考<a href="/zh/blog/2024/build-own-chatgpt-on-azure/#%E5%85%88%E5%86%B3%E6%9D%A1%E4%BB%B6">之前文章中的注册 Azure 订阅内容</a>进行操作。</p>
<h3 id="创建-azure-ai-foundry">创建 Azure AI Foundry</h3>
<p>首先进入您的 Azure 订阅中的 AI Foundry，展开左侧 All Resources，找到 Azure AI Foundry，点击 Create 创建一个 Azure AI Foundry：</p>
<p><img alt="创建 Azure AI Foundry" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/1-azure-ai-foundry.png"></p>
<p>创建时注意区域选择，由于 Sora 2 模型并未在所有 Azure 区域开放预览，这里建议选择 East US 2 区域：</p>
<p><img alt="选择 AI Foundry 区域" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/2-create-ai-foundry.png"></p>
<h3 id="1-创建-azure-ai-foundry-project">1. 创建 Azure AI Foundry Project</h3>
<p>创建完成后进入您的 Azure AI Foundry 在 All Resources 中找到 Projects，点击 New 创建一个新的 Project：</p>
<p><img alt="创建 Project" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/3-azure-ai-foundry-project.png"></p>
<h3 id="2-打开-azure-ai-foundry-protal">2. 打开 Azure AI Foundry Protal</h3>
<p>创建完成 Project 后，打开您的 Project，点击 Go to Azure AI Foundry protal：</p>
<p><img alt="打开 AI Foundry Portal" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/4-go-to-portal.png"></p>
<p>正常情况下 Azure AI Foundry Protal 如下：</p>
<p><img alt="AI Foundry Portal" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/5-ai-foundry-portal.png"></p>
<h3 id="3-启用和部署-sora-2-模型">3. 启用和部署 Sora 2 模型</h3>
<p>又有 Sora 2 模型在本文发布时仍然处于预览状态，所以我们首先需要申请权限，后续开放使用后可调过本步骤，首先点击左侧的 Model catalog，可以看到 Sora 2 模型的标签，点击 Check out model：</p>
<p><img alt="Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/6-check-out-sora-2.png"></p>
<p>也可以在下方的模型列表找到想要的模型进行使用。</p>
<p>如果您没有使用 Sora 2 模型的权限，进入 Sora 2 模型介绍界面后将如下所示：</p>
<p><img alt="获取 Sora 2 模型权限" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/7-request-access-sora-2.png"></p>
<p>请点击 Request access 链接进行权限申请。</p>
<p>如您已经拥有 Sora 2 访问权限，可以点击 Use this model 进行模型部署：</p>
<p><img alt="使用 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/8-use-sora-2.png"></p>
<p>部署过程非常简单，默认保持各类选项即可进行创建部署：</p>
<p><img alt="部署 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/9-deploy-sora-2.png"></p>
<p>部署完成后，切换左侧菜单至 Models + endpoints，查看模型列表：</p>
<p><img alt="查看 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/10-models-sora-2.png"></p>
<p>至此您就拥有了属于自己的 Sora 2 模型，可以开始使用！</p>
<h2 id="通过-azure-ai-foundry-playground-使用-sora-2">通过 Azure AI Foundry Playground 使用 Sora 2</h2>
<p>Azure AI Foundry 提供了强大的可视化界面，你可以无需写代码就生成视频。</p>
<h3 id="1-进入-playground">1. 进入 Playground</h3>
<p>打开 Azure AI Foundry 左侧菜单中的 Playgrounds，选择 Video playground：</p>
<p><img alt="Video Playground" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/11-video-playground.png"></p>
<p>进入 Playground 后选择 Sora 2 模型：</p>
<p><img alt="选择 Sora 2 模型" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/12-choose-sora-2.png"></p>
<h3 id="2-填写提示词-prompt">2. 填写提示词 Prompt</h3>
<p>示例 Prompt：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>A high-energy cyberpunk chase in the rain with neon reflections on wet streets. A futuristic motorcycle speeds through, dynamic motion blur capturing its sleek design and vibrant city lights.
</span></span></code></pre></div><p><img alt="输入提示词" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/13-input-prompt.png"></p>
<h3 id="3-设置选项">3. 设置选项</h3>
<p>可调节选项包括：</p>
<table>
	<thead>
			<tr>
					<th>选项</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Attach an image</strong></td>
					<td>可以附加图片进行相视频的生成</td>
			</tr>
			<tr>
					<td><strong>Aspect ratio</strong></td>
					<td>视频长宽比，默认为 16:9</td>
			</tr>
			<tr>
					<td><strong>Resolution</strong></td>
					<td>分辨率，当前只提供 720p</td>
			</tr>
			<tr>
					<td><strong>Duration</strong></td>
					<td>输出视频时长（如 5s、10s），默认 8s</td>
			</tr>
	</tbody>
</table>
<p><img alt="设置生成参数" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/14-settings-sora-2.png"></p>
<h3 id="4-生成视频">4. 生成视频</h3>
<p>设置好后，点击 Generate 进行视频生成，视频生成成功后，你可以在 Generation history 中预览或下载视频：</p>
<p><img alt="生成视频" loading="lazy" src="/zh/blog/2025/azure-ai-foundry-sora-2/15-video.png"></p>
<h2 id="通过-python-调用-sora-2">通过 Python 调用 Sora 2</h2>
<p>Azure 为 OpenAI 模型提供统一的接口，可以直接在你的后端调用。</p>
<h3 id="1-安装-sdk">1. 安装 SDK</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install azure-ai-openai
</span></span><span style="display:flex;"><span>pip install python-dotenv
</span></span></code></pre></div><h3 id="2-配置环境变量">2. 配置环境变量</h3>
<p>创建 <code>.env</code> 文件：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>AZURE_OPENAI_ENDPOINT<span style="color:#f92672">=</span>https://&lt;your-resource-name&gt;.openai.azure.com/
</span></span><span style="display:flex;"><span>AZURE_OPENAI_KEY<span style="color:#f92672">=</span>&lt;your-key&gt;
</span></span><span style="display:flex;"><span>AZURE_OPENAI_API_VERSION<span style="color:#f92672">=</span>2024-10-01-preview
</span></span></code></pre></div><h3 id="3-python-调用示例文生视频">3. Python 调用示例：文生视频</h3>
<p>下面是最简可运行示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> dotenv <span style="color:#f92672">import</span> load_dotenv
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.ai.openai <span style="color:#f92672">import</span> OpenAIClient
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> azure.core.credentials <span style="color:#f92672">import</span> AzureKeyCredential
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>load_dotenv()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAIClient(
</span></span><span style="display:flex;"><span>    endpoint<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_ENDPOINT&#34;</span>),
</span></span><span style="display:flex;"><span>    credential<span style="color:#f92672">=</span>AzureKeyCredential(os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_KEY&#34;</span>)),
</span></span><span style="display:flex;"><span>    api_version<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#34;AZURE_OPENAI_API_VERSION&#34;</span>)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">A cinematic shot of a spaceship flying through a nebula filled with colorful cosmic clouds.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>get_video(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sora-2&#34;</span>,
</span></span><span style="display:flex;"><span>    prompt<span style="color:#f92672">=</span>prompt,
</span></span><span style="display:flex;"><span>    duration<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span>,              <span style="color:#75715e"># seconds</span>
</span></span><span style="display:flex;"><span>    resolution<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1080p&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 保存视频</span>
</span></span><span style="display:flex;"><span>video_bytes <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>video
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> open(<span style="color:#e6db74">&#34;output.mp4&#34;</span>, <span style="color:#e6db74">&#34;wb&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>    f<span style="color:#f92672">.</span>write(video_bytes)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;Video generated: output.mp4&#34;</span>)
</span></span></code></pre></div><h3 id="4-使用结构化-payload">4. 使用结构化 payload</h3>
<p>Sora 还支持更多控制，例如镜头、分镜头脚本、环境细节。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;prompt&#34;</span>: <span style="color:#e6db74">&#34;A cute corgi running on a beach during sunset.&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;duration&#34;</span>: <span style="color:#ae81ff">8</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;resolution&#34;</span>: <span style="color:#e6db74">&#34;720p&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;motion&#34;</span>: <span style="color:#e6db74">&#34;medium&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;style&#34;</span>: <span style="color:#e6db74">&#34;realistic&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>response <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>get_video(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sora-2&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">**</span>payload
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">with</span> open(<span style="color:#e6db74">&#34;corgi_beach.mp4&#34;</span>, <span style="color:#e6db74">&#34;wb&#34;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>    f<span style="color:#f92672">.</span>write(response<span style="color:#f92672">.</span>video)
</span></span></code></pre></div><h2 id="生成最佳视频的技巧">生成最佳视频的技巧</h2>
<h3 id="1-提示词结构推荐general-prompt-template">1. 提示词结构推荐（General Prompt Template）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[主体对象] + [场景环境] + [动作] + [镜头效果] + [光影] + [风格]
</span></span></code></pre></div><p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>A high-energy cyberpunk chase in the rain with neon reflections on wet streets. A futuristic motorcycle speeds through, dynamic motion blur capturing its sleek design and vibrant city lights.
</span></span></code></pre></div><h3 id="2-控制一致性">2. 控制一致性</h3>
<ul>
<li>使用 <strong>seed</strong> 可重复生成风格一致的视频</li>
<li>合理设置 motion level 获得更自然的动作</li>
<li>提供清晰场景描述可减少模型误解</li>
</ul>
<h2 id="常见问题faq">常见问题（FAQ）</h2>
<h3 id="1-我试了但看不到-sora-2">1. 我试了但看不到 Sora 2？</h3>
<p>目前需要访问权限。你需要：</p>
<ul>
<li>联系 Azure 管理员</li>
<li>到模型页面确认已启用</li>
</ul>
<h3 id="2-视频最大时长是多少">2. 视频最大时长是多少？</h3>
<p>具体限制依区域会有不同，通常为 <strong>≤ 10 秒</strong>。</p>
<h3 id="3-python-生成的视频过小或无内容">3. Python 生成的视频过小或无内容？</h3>
<p>确保保存时使用 <code>response.video</code> 而不是错误字段。</p>
<h2 id="总结">总结</h2>
<p>本文介绍了在 <strong>Azure AI Foundry</strong> 中使用 <strong>Sora 2 文生视频模型</strong>的两种方式：</p>
<ul>
<li>使用可视化 <strong>Playground</strong> 快速体验</li>
<li>使用 <strong>Python SDK</strong> 在生产环境自动生成视频</li>
</ul>
<p>Azure 的企业级安全合规能力让 Sora 2 成为企业落地视频生成的重要选择。</p>
]]></content:encoded></item><item><title>AI 推理的最佳选择 - vLLM</title><link>https://wilsonwu.me/zh/blog/2025/the-best-ai-inference-vllm/</link><pubDate>Wed, 15 Oct 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/the-best-ai-inference-vllm/</guid><description>随着各组织从大语言模型（LLM）的试验阶段迈向生产部署，选择哪种推理平台就成了一项关键的业务决策。这个选择不仅影响性能，也影响灵活性、成本优化，以及应对快速变化业务需求的能力。</description><content:encoded><![CDATA[<p>原文链接：<a href="https://medium.com/open-5g-hypercore/episode-xxx-the-best-choice-for-ai-inference-vllm-286b2af2df71">The Best Choice for AI Inference -&gt; vLLM</a></p>

<blockquote>
  <p>注意：本译文已获得的原文作者翻译授权，为避免广告嫌疑，本文中移除了特定产品和品牌的商业宣传内容，仅保留技术特性及企业级产品功能描述，相关内容请以原文为准。</p>

</blockquote>
<p><img alt="AI 推理的最佳选择 - vLLM" loading="lazy" src="/zh/blog/2025/the-best-ai-inference-vllm/1-vllm.png"></p>
<p>随着各组织从大语言模型（LLM）的试验阶段迈向生产部署，选择哪种推理平台就成了一项关键的业务决策。这个选择不仅影响性能，也影响灵活性、成本优化，以及应对快速变化业务需求的能力。</p>
<p>对于技术人员和方案架构师在评估 LLM 推理平台时，应该重点考虑以下三大因素：</p>
<ol>
<li><strong>架构灵活性</strong>：能否在不同硬件加速器和混合云环境间部署，而不会被某一家厂商锁定。</li>
<li><strong>运行可扩展性</strong>：支持从单 GPU 部署扩展到分布式多节点的高级部署模式。</li>
<li><strong>生态开放性</strong>：对最广泛的模型与内核支持，以及能与各种企业软件生态系统整合。</li>
</ol>
<p>vLLM 在开源基础、先进内存管理能力，以及即将推出的分布式部署蓝图方面，独特地满足这些需求。与专有或硬件专用方案不同，这套组合提供了在成本、性能和运营需求上随时优化调整的自由。</p>
<p>本文将深入分析为何 vLLM 在其技术架构与能力上（尤其是其 KV-Cache 管理、并行策略，以及未来的 llm-d 分布式能力）提供了最可持续的生产级 LLM 部署路径。</p>
<hr>
<h2 id="开源优势">开源优势</h2>
<h3 id="社区驱动的大规模创新">社区驱动的大规模创新</h3>
<p>LLM 推理的发展，根本上受到开源创新的推动。过去一年半以来 <a href="https://blog.vllm.ai/2025/01/27/v1-alpha-release.html">vLLM V1: A Major Upgrade to vLLM’s Core Architecture | vLLM 博客（英文版）</a>，vLLM 在支持多样模型、功能和硬件后端方面取得显著成绩，从伯克利大学的研究项目成长为开源 AI 生态中的事实标准之一 <a href="https://blog.vllm.ai/2025/01/10/vllm-2024-wrapped-2025-vision.html">vLLM 2024 Retrospective and 2025 Vision | vLLM 博客（英文版）</a>。</p>
<p><img alt="vLLM 社区发展" loading="lazy" src="/zh/blog/2025/the-best-ai-inference-vllm/2-vllm-growth.png"></p>

<blockquote>
  <p><a href="https://inferenceops.substack.com/p/state-of-the-model-serving-communities-269">vLLM 社区发展参考链接</a></p>

</blockquote>
<p>vLLM 现在隶属于 PyTorch 基金会托管项目（<a href="https://github.com/vllm-project/vllm">GitHub — vllm-project/vllm: A high-throughput and memory-efficient inference and serving engine for LLMs</a>），这保证了其长远的可持续性和治理机制。</p>
<h3 id="企业支持与开源结合">企业支持与开源结合</h3>
<p>许多企业及产品及解决方案对 vLLM 的态度类似于其对 Linux/OpenStack/Kubernetes 的策略：在社区驱动创新基础上，加入企业级支持、安全性和运维工具。这样模型即可部署在本地、公有云或混合云中，避免绑定。</p>
<p>这种方式带来的好处包括：</p>
<ul>
<li><strong>无厂商绑定</strong>：用户可自由选择部署环境</li>
<li><strong>透明开发</strong>：安全漏洞与缺陷公开追踪与快速修复</li>
<li><strong>社区贡献共享</strong>：一个组织开发的功能惠及整个生态</li>
<li><strong>可定制性</strong>：源代码开放，可针对特定需求修改</li>
</ul>
<p>此外，vLLM 在以下方面具备战略优势：</p>
<ul>
<li><strong>硬件无关性</strong>：不局限于 NVIDIA，支持 AMD、Intel、TPU 等多种平台（<a href="https://github.com/vllm-project/vllm">GitHub — vllm-project/vllm: A high-throughput and memory-efficient inference and serving engine for LLMs</a>）</li>
<li><strong>快速功能迭代</strong>：vLLM v1 对其核心模块（调度器、KV 缓存管理、worker、采样器、API 服务等）进行了全面重构（<a href="https://blog.vllm.ai/2025/01/27/v1-alpha-release.html">vLLM V1: A Major Upgrade to vLLM’s Core Architecture | vLLM 博客（英文版）</a>）</li>
<li><strong>生态整合</strong>：原生兼容 Hugging Face、OpenAI API、Kubernetes 生态</li>
<li><strong>成本优化</strong>：可以为不同工作负载选择最具性价比的硬件</li>
</ul>
<hr>
<h2 id="架构灵活性与并行化策略">架构灵活性与并行化策略</h2>
<h3 id="理解并行化understanding-parallelization">理解并行化（Understanding Parallelization）</h3>
<p>大型语言模型带来了特殊的扩展挑战。例如：一个拥有 70B 参数 的模型，仅其权重（使用 FP16 精度）就需约 140GB 内存，这远远超过单一卡片的容量。基于 vLLM 及相关产品和解决方案通过四种互补的并行化策略来应对这些挑战，每一种策略都解决不同类型的扩展问题。</p>
<h3 id="数据并行data-parallelism-dp跨请求扩展">数据并行（Data Parallelism, DP）：跨请求扩展</h3>
<p>数据并行是最简单的一种扩展模式：在多个服务器上运行完整模型的多个副本，每个副本处理不同的请求批次。在这种模式下：</p>
<ul>
<li><strong>维护完整模型</strong>：每个加速器／服务器保存整个模型的权重。</li>
<li><strong>分配请求</strong>：请求由负载均衡器分发给不同的副本。</li>
<li><strong>支持线性扩展</strong>：增加服务器数目可以线性提升吞吐量。</li>
<li><strong>简化部署</strong>：部署相对简单，不需要对模型进行切分（sharding）。</li>
</ul>
<pre tabindex="0"><code class="language-code" data-lang="code">          [负载均衡器]
               |
    ------------------------
    |          |           |
[Server1]  [Server2]  [Server3]
[Model A]  [Model A]  [Model A]
    |          |           |
[Batch 1]  [Batch 2]  [Batch 3]

关键：每台服务器都有完整的模型副本并行处理不同的请求
</code></pre><p>此模式与基于 KServe 的 Replica-Sets 用于模型副本服务很好的契合，能自动根据请求负载进行扩展，同时保有模型服务的简单性。</p>
<h3 id="流水线并行pipeline-parallelism-pp按层分配">流水线并行（Pipeline Parallelism, PP）：按层分配</h3>
<p>流水线并行将模型按照层（layers）分拆，不同的加速器处理不同的网络层：</p>
<ul>
<li><strong>顺序处理</strong>：请求在 GPU 间依次通过模型层进行处理。</li>
<li><strong>内存均衡</strong>：内存需求在多个设备间较为均匀分布。</li>
<li><strong>灵活部署</strong>：部署灵活，可以跨多个节点，不一定依赖特别高速的互联。</li>
<li><strong>微批处理</strong>：通过微批处理（micro-batching）来保持 GPU 利用率。</li>
</ul>
<pre tabindex="0"><code class="language-code" data-lang="code">Request → [GPU1:NN-Layers 1-8] → [GPU2:NN-Layers 9-16] → [GPU3:NN-Layers 17-24] → Response
             ↓                     ↓                      ↓
          [Memory]              [Memory]               [Memory]
          
时间线：请求按顺序流经 GPU
</code></pre><p>但流水线并行在现代 LLM 应用中也面临一些限制：</p>
<ul>
<li><strong>“流水线气泡”（pipeline bubble）效率低下</strong>：某些 GPU 在等待上游阶段的数据时处于空闲状态。</li>
<li><strong>通信开销高</strong>：阶段间数据传输成为瓶颈。</li>
<li><strong>延迟敏感</strong>：每个阶段都会增加延迟，对“第一个 token 的时间”（time-to-first-token）有负面影响。</li>
<li><strong>对 MoE（Mixture of Experts，混合专家模型）不友好</strong>：MoE 模型中只激活某些专家（非密集执行），这样无法很好地映射到按层划分的流水线结构中。</li>
</ul>
<p>因为这些限制，很多生产环境更倾向于将张量并行（Tensor Parallelism）用于节点内部扩展，将数据并行（Data Parallelism）用于跨节点扩展。尽管如此，在内存受限或与其他并行策略组合使用的场景中，流水线并行仍然有价值。</p>
<h3 id="张量并行tensor-parallelism-tp切分模型权重">张量并行（Tensor Parallelism, TP）：切分模型权重</h3>
<p>当模型太大，单个加速器无法容纳其所有权重时，就使用张量并行来将模型权重拆分到多个 GPU 上：</p>
<ul>
<li><strong>水平层拆分</strong>：每次矩阵乘法操作被分配到多个 GPU，按部分计算。</li>
<li><strong>同步计算</strong>：GPU 之间通过高速互联（如 NVLink、Infinity Fabric 等）进行同步和数据交换。</li>
<li><strong>内存效率</strong>：可使得模型可服务的规模比单个 GPU 容量大出约 4～8 倍。</li>
<li><strong>低延迟</strong>：如果硬件条件合适，通信开销低的话，张量并行还能保持较低的延迟。</li>
</ul>
<pre tabindex="0"><code class="language-code" data-lang="code">Original Matrix Operation:
[Large Weight Matrix] × [Input] = [Output]

With Tensor Parallelism:
[Weight Part 1] → GPU1 ↘
[Weight Part 2] → GPU2 → [Combine] → [Output]
[Weight Part 3] → GPU3 ↗

关键：单矩阵运算跨 GPU 拆分，需要高速互连（NVLink）
</code></pre><p>vLLM 的张量并行实现是硬件无关的，支持多种互联技术和不同类型的加速器。</p>
<h3 id="专家并行expert-parallelism-epmoe-模型中的专家分布策略">专家并行（Expert Parallelism, EP）：MoE 模型中的专家分布策略</h3>
<p>对于<strong>混合专家模型（MoE）</strong>，专家并行把不同的专家（experts）分配给多个 GPU 或多个节点，而不是让每个 GPU 都保存所有专家，由一个路由层（router layer）动态地将 token 分发给需要的专家：</p>
<ul>
<li><strong>分布式专家分片</strong>：专家（experts）被分片（shard）分布在不同的 GPU／节点，使拥有上百个专家的模型能扩展到远超过单设备的内存容量。</li>
<li><strong>动态 token 路由</strong>：每个 token 只被发送到它应使用的一个或多个专家，以减少计算开销。</li>
<li><strong>专家并行负载平衡（Expert Parallel Load Balancing, EPLB）</strong>：防止某些热门专家／专家组被频繁调用导致不均匀负载，通过动态复制或分布专家，以分散负载。</li>
<li><strong>分层调度（Hierarchical Scheduling）</strong>：在多节点集群中，路由与复制先在节点间协调，再在节点内部的 GPU 间协调，确保整体利用率均衡且减少跨节点通信。</li>
<li><strong>性能收益</strong>：对大型的 MoE 模型，这种专家并行在具有高速互联（NVLink、InfiniBand 等）的环境中可以实现接近线性扩展的吞吐效率。</li>
</ul>
<pre tabindex="0"><code class="language-code" data-lang="code">               [Router Layer]
                      |
        ---------------------------------
        |          |          |         |
      GPU1        GPU2       GPU3      GPU4
    [Exp 1,2]   [Exp 3,4]  [Exp 5,6]  [Exp 7,8]
    
令牌流：路由器 → 选定专家 → 输出
</code></pre><h3 id="vllm-的统一策略">vLLM 的统一策略</h3>
<p>vLLM 的区别与优势在于它能够无缝结合这些并行策略，为分布式推理提供高度灵活性，使组织能够：</p>
<ol>
<li><strong>混合使用策略</strong>：例如，在节点内部用张量并行，在节点间用流水线并行。</li>
<li><strong>根据硬件特性调整</strong>：基于可用的互联速度、GPU 内存、延迟敏感性等因素做最优规划。</li>
<li><strong>动态扩展</strong>：可以从单节点部署起步，随着需求增长扩展到多节点／多 GPU。</li>
<li><strong>保持 API 的一致性</strong>：无论使用哪种并行策略，道线路径、调用接口等对使用者尽量透明，不需要因为并行方式不同而改很多逻辑。</li>
</ol>
<p>这种灵活性在混合云环境中尤其关键，因为不同环境（本地、云端、不同厂商硬件）之间的配置可能大不相同。</p>
<pre tabindex="0"><code class="language-code" data-lang="code">Hybrid TP+PP+EP+DP Deployment (MoE Model):

Pipeline Stage 1:               Pipeline Stage 2:              Pipeline Stage 3:
Node 1 (Attention Layers)       Node 2 (MoE FFN Layers)        Node 3 (Output Layers)
[Attn Part A] ←→ [Attn Part B]  [Router + Experts Subset]      [Out Part A] ←→ [Out Part B]
    GPU1           GPU2          GPU3: Experts 1-4                  GPU5            GPU6
         ↕ NVLink ↕              GPU4: Experts 5-8                       ↕ NVLink ↕
                                     ↕ NVLink ↕

Expert Distribution Detail (Node 2):
            [Router]
         GPU3  |  GPU4
        -------|-------
        Exp1   |  Exp5
        Exp2   |  Exp6  
        Exp3   |  Exp7
        Exp4   |  Exp8

Request Flow: → [Attention] → [MoE Layer/Router] → [Output] → Response
                              ↓
                    [Route to specific experts]

Parallelization Breakdown:
- Tensor Parallel (TP): Attention and Output layers split within nodes.
- Pipeline Parallel (PP): Different model stages across nodes .
- Expert Parallel (EP): MoE experts distributed across GPUs in Node 2.
- Data Parallel (DP): Entire pipeline can be replicated for more throughput.
</code></pre><hr>
<h2 id="更好的内存管理kv-cachekey-value-缓存">更好的内存管理：KV-Cache（Key-Value 缓存）</h2>
<h3 id="在-llm-推理中理解-kv-cache">在 LLM 推理中理解 KV-Cache</h3>
<p>KV-Cache（键-值缓存）是 LLM 推理优化中最关键的特性之一。在注意力计算期间，模型必须访问先前 token 的表示 —— 随着序列长度增长，这个过程变得非常消耗内存。</p>
<p>高效的 KV-Cache 管理能决定同一硬件上能同时服务 10 个并发用户或 100 个并发用户，因为缓存复用将计算换成内存利用率。</p>
<pre tabindex="0"><code class="language-code" data-lang="code">(Q * K^T) * V computation process with caching

Step 1 (Prefill Phase)
                    Keys Transpose
                          ↓
    Queries              Keys              Values            Results
       |                  |                  |                  |
    [  Q  ]      ×     [  K  ]      →    [Scores]     ×    [  V  ]    →    [Output]
       |                  |                  |                  |
      64                 64                                    64              64
                          ↓                                     ↓
                    [Storing K]                           [Storing V]
                          ↓                                     ↓
                    ←── Cache memory (stores both K and V) ──→

Step N (Decode Phase)
                    Keys Transpose
                          ↓
    Query              Cached K            Cached V          Results
       |                  |                  |                  |
    [  q  ]      ×   [K₁...Kₙ]      →    [Scores]     ×   [V₁...Vₙ]   →    [output]
    (new)             (from cache)                         (from cache)       (new)
       |                  |                  |                  |
       1                 64                                    64               1
                          ↑                                     ↑
                    ←── Reading from cache (both K and V) ──→

Key Points:
- Prefill: Computes K and V for all input tokens, stores both in cache
- Decode: For each new token, reads ALL previous K and V values from cache
- Both K and V matrices must be cached (not just V)
- Cache size grows with sequence length
</code></pre><h3 id="prefill-阶段-vs-decode-阶段两个截然不同的阶段">Prefill 阶段 vs Decode 阶段：两个截然不同的阶段</h3>
<p>LLM 推理包含两个本质不同的阶段：</p>
<h4 id="prefill-阶段prompt-处理阶段">Prefill 阶段（Prompt 处理阶段）</h4>
<ul>
<li>并行处理所有输入 token。</li>
<li>计算密集型，GPU 利用率高。</li>
<li>为 prompt 中的所有 token 生成初始的 KV-Cache 条目。</li>
<li>延迟与 prompt 长度成正比。</li>
<li>大批量（larger batch sizes）带来好处。</li>
</ul>
<h4 id="decode-阶段token-生成阶段">Decode 阶段（Token 生成阶段）</h4>
<ul>
<li>一个 token 一个 token 顺序生成。</li>
<li>此阶段受限于内存带宽（memory-bandwidth bound）。</li>
<li>每新增一个 token，都要读取整个 KV-Cache 来自缓存的所有先前的 K 和 V。</li>
<li>延迟与输出 token 数量成正比。</li>
<li>依赖高效的缓存管理。</li>
</ul>
<h3 id="pagedattentionvllm-的内存革命">PagedAttention：vLLM 的内存革命</h3>
<p>vLLM 引入 PagedAttention，这是一种在 KV-Cache 管理方面的突破性设计，它将 GPU 内存视为类似操作系统中的虚拟内存（virtual memory）来处理，其关键设计包括：</p>
<ul>
<li><strong>非连续存储（Non-contiguous Storage）</strong>：KV-Cache 块可以存储在 GPU 内存的任意地方，不必是连续的。</li>
<li><strong>动态分配（Dynamic Allocation）</strong>：随着序列长度增长时才分配内存。</li>
<li><strong>内存共享（Memory Sharing）</strong>：完全相同的 prompt 前缀（prompt prefixes）可以共享 KV-Cache 块。</li>
<li><strong>几乎零浪费（Near-Zero Waste）</strong>：消除静态分配中常见的内部碎片问题。</li>
</ul>
<p>这种设计让 vLLM 可以维持更大的批量大小、更高的并发性、以及比那些依赖静态、大型 KV-Cache 缓冲区的系统更好的 GPU 利用率。</p>
<h3 id="连续批处理continuous-batching最大化-gpu-利用率">连续批处理（Continuous Batching）：最大化 GPU 利用率</h3>
<p>vLLM 的连续批处理机制带来了几个举措：</p>
<ul>
<li><strong>动态请求加入</strong>：新的请求可以在 decode 步骤之间加入正在运行的 batch。</li>
<li><strong>提前完成处理</strong>：已经完成的序列立即释放资源。</li>
<li><strong>GPU 使用最优化</strong>：通过混合处理 prefill 与 decode 操作以保持高利用率。</li>
<li><strong>支持抢占（Preemption）</strong>：可以暂停低优先级的请求以优先处理紧急请求。</li>
</ul>
<h3 id="在部署中的实际意义">在部署中的实际意义</h3>
<p>这些内存管理上的创新在实际部署中带来了具体的效益：</p>
<ol>
<li><strong>更高的并发性</strong>：在相同硬件上服务更多用户。</li>
<li><strong>更佳的成本效率</strong>：大幅减少基础设施需求。</li>
<li><strong>改善延迟</strong>：通过高效调度缩短 time-to-first-token。</li>
<li><strong>灵活性</strong>：可以处理变长的序列长度，而无需重新配置系统。</li>
</ol>
<p>KV-Cache 优化在即将推出的 llm-d 分布式架构中变得尤为关键，因为高效的内存使用使得过去因内存约束而无法实现的新部署模式成为可能。</p>
<hr>
<h2 id="使用-llm-d-扩展kubernetes-原生的分布式推理">使用 llm-d 扩展：Kubernetes 原生的分布式推理</h2>
<h3 id="超越单服务器部署">超越单服务器部署</h3>
<p>虽然 vLLM 自身是高性能推理引擎，但在大规模生产环境中，还需要复杂的编排和智能请求调度。2025 年 5 月，红帽、Google Cloud、IBM 研究院、NVIDIA 和 CoreWeave 联手推出 llm-d，构建在 vLLM 之上的 Kubernetes 原生分布式推理栈。</p>
<p>llm-d 并不是 vLLM 的内置功能，而是一个补充的编排层。可以把它比作 Linux 与 Kubernetes 的关系：vLLM 负责推理引擎，llm-d 负责跨实例的调度与编排。它结合了三项开源技术：vLLM、Inference Gateway（AI 路由层）和 Kubernetes。</p>
<p>KServe 也在集成 llm-d，通过新的 <a href="https://docs.google.com/document/d/11ZQJ2VhTc42S9K4yau2dMs3Q3f4jqWJL_7Sq14C3hzY/edit?tab=t.0">LLMInference CRD</a>，实现统一的服务接口，简化生成式 AI 的企业上手路径。</p>
<p><img alt="Kubernetes 中的 AI 推理" loading="lazy" src="/zh/blog/2025/the-best-ai-inference-vllm/3-ai-inference-k8s.png"></p>
<p>llm-d 将三大基础开源技术集成到一个统一的服务栈中：</p>
<ul>
<li><strong>vLLM</strong> - 执行模型推理的高性能推理引擎。</li>
<li><strong>推理网关（IGW）</strong> - 一个官方 Kubernetes 项目，通过 AI 感知路由扩展了 Gateway API。</li>
<li><strong>Kubernetes</strong> - 用于部署和扩展的行业标准编排平台。</li>
</ul>
<p>通过整合这些技术，llm-d 使企业能够在混合云环境中大规模部署 LLM 推理，并实现最快的价值实现时间和极具竞争力的性价比。</p>
<h3 id="三项关键创新">三项关键创新</h3>
<p>传统的负载均衡采用简单的轮询（round-robin）路由，对所有服务器一视同仁。llm-d 的 <strong>vLLM 感知调度程序</strong>能够做出智能决策，将请求路由到具有匹配缓存前缀的实例，根据实例是处理计算密集型预填充操作还是内存密集型解码操作来分配负载，并使用来自 vLLM 的实时可观测数据来避免实例过载，同时优先处理低延迟路径。这种智能路由可将基础设施成本降低 30% 到 50%，同时保持延迟 SLO。</p>
<h4 id="解耦式服务disaggregated-serving">解耦式服务（Disaggregated Serving）</h4>
<p>llm-d 在生产规模上协调 vLLM 的本机分解服务（通过 KVConnector API），将预填充和解码分离到专门的工作节点中：</p>
<ul>
<li>
<p><strong>预填充工作节点</strong>：负责计算密集型的提示处理，使用高性能 GPU（如 H100、MI300X），并可根据需求独立扩展。</p>
</li>
<li>
<p><strong>解码工作节点</strong>：专注于受内存带宽限制的 token 生成任务，使用具成本效益的 GPU（如 A100、L40S），并根据并发会话数进行弹性扩缩。</p>
</li>
<li>
<p><strong>KV 缓存传输</strong>：使用 NVIDIA NIXL 通过 UCX 提供高效的缓存移动，支持卸载到存储后端（未来交付）以及跨集群的全局缓存感知。</p>
</li>
</ul>
<p>这允许适当规模的基础设施：昂贵的 GPU 仅用于预填充，成本优化的硬件可为数千个并发用户提供服务。</p>
<h4 id="分布式前缀缓存distributed-prefix-caching">分布式前缀缓存（Distributed Prefix Caching）</h4>
<p>llm-d 将 vLLM 的前缀缓存扩展到多个实例之间，通过两种方式实现：</p>
<ul>
<li>
<p><strong>本地缓存</strong>：在每个实例上将缓存卸载（offload）到内存或磁盘，几乎没有运维成本。</p>
</li>
<li>
<p><strong>共享缓存（计划中）</strong>：通过实例之间的 KV 传输实现共享，并使用全局索引实现集群范围的缓存感知。</p>
</li>
</ul>
<hr>
<h4 id="部署模式deployment-patterns">部署模式（Deployment Patterns）</h4>
<p>llm-d 支持多种先进的企业部署模式：</p>
<ul>
<li>
<p><strong>异构硬件</strong>：根据工作负载混合 GPU 供应商和代数——用于预填充的高端 GPU、用于解码的成本优化 GPU 或用于低频请求的 CPU 集群。</p>
</li>
<li>
<p><strong>动态扩缩</strong>：在高峰时段独立调整预填充容量，同时保持活动会话的稳定解码容量，并具有自动资源分配和故障转移功能。</p>
</li>
<li>
<p><strong>地理分布（规划中）</strong>：在主数据中心部署集中式预填充工作者，并在用户附近部署边缘解码工作者，以实现低延迟响应。</p>
</li>
</ul>
<hr>
<h2 id="多加速器与混合云支持">多加速器与混合云支持</h2>
<h3 id="摆脱硬件锁定">摆脱硬件锁定</h3>
<p>AI 加速器快速演进，使市场变得多样化。专优化 NVIDIA 的方案如 TensorRT-LLM 虽然性能优秀，但容易导致锁定。vLLM 的硬件中立设计则允许用户自由选择最适合的加速器。</p>
<h3 id="广泛硬件支持">广泛硬件支持</h3>
<p>vLLM 支持 NVIDIA GPU（针对 H100 进行一流的优化，支持 V100 及更新版本的所有 NVIDIA GPU）、AMD GPU（MI200、MI300 和 Radeon RX 7900 系列）、Google TPU（v4、v5p、v5e 和最新的 v6e）、AWS Inferentia 和 Trainium（trn1/inf2 实例）、Intel Gaudi（HPU）和 GPU（XPU）以及支持 x86、ARM 和 PowerPC <a href="https://blog.vllm.ai/2025/01/10/vllm-2024-wrapped-2025-vision.html">vLLM 2024 Retrospective and 2025 Vision | vLLM 博客（英文版）</a>。</p>
<p>这种广泛支持带来以下战略优势：</p>
<h4 id="成本优化">成本优化</h4>
<ul>
<li>选择 AMD MI300X，在特定工作负载下实现高性价比。</li>
<li>使用 AWS Inferentia，在 AWS 上进行经济高效的推理。</li>
<li>在低吞吐量用例中，可部署在现有 CPU 基础架构上。</li>
</ul>
<h4 id="供应链弹性">供应链弹性</h4>
<ul>
<li>避免依赖单一 GPU 供应商的可用性。</li>
<li>通过多种选择协商更优惠的价格。</li>
<li>根据区域硬件可用性进行调整。</li>
</ul>
<h4 id="工作负载匹配">工作负载匹配</h4>
<ul>
<li>NVIDIA H100 可实现最佳性能。</li>
<li>AMD GPU 适用于开源部署。</li>
<li>TPU 适用于 Google Cloud 部署。</li>
<li>Intel Gaudi 适用于特定企业协议。</li>
</ul>
<h3 id="混合云部署模式">混合云部署模式</h3>
<p>在企业级产品及解决方案中，vLLM 支持真正的混合云：</p>
<h4 id="本地核心">本地核心</h4>
<ul>
<li>在本地基础架构上处理敏感数据。</li>
<li>满足合规性要求的工作负载。</li>
<li>可预测的基线负载容量。</li>
</ul>
<h4 id="云爆发式扩展">云爆发式扩展</h4>
<ul>
<li>利用云资源处理峰值负载。</li>
<li>试用新硬件（H100s、TPU v6e）。</li>
<li>无需基础设施投资即可实现地域扩展。</li>
</ul>
<h4 id="边缘推理">边缘推理</h4>
<ul>
<li>部署在适合边缘计算的硬件上。</li>
<li>CPU 或更小的 GPU 推理。</li>
<li>与集中管理集成。</li>
</ul>
<hr>
<h2 id="统一运维跨环境">统一运维跨环境</h2>
<p>借助企业级产品的相关功能，用户可从单一控制平面管理全部部署（公有云 + 私有云），主要从一下几个方面考虑。</p>
<ul>
<li><strong>单一控制平面</strong>：通过统一界面管理所有部署。</li>
<li><strong>一致的 API</strong>：跨环境实现相同的应用程序集成。</li>
<li><strong>统一监控</strong>：跨混合部署的聚合指标。</li>
<li><strong>策略执行</strong>：一致的安全和合规策略。</li>
</ul>
<hr>
<h2 id="模型生态与兼容性">模型生态与兼容性</h2>
<h3 id="支持广泛的开源模型体系">支持广泛的开源模型体系</h3>
<p>随着人工智能“淘金热”步伐的加快，vLLM 已发展到支持<a href="https://docs.vllm.ai/en/stable/models/supported_models.html#text-generation">超过 100 种模型架构</a>的高性能推理：涵盖几乎所有著名的开源大型语言模型（LLM）、多模态模型（图像、音频、视频）、编码器-解码器模型、推测解码模型、分类模型、嵌入模型和奖励模型。<a href="https://blog.vllm.ai/2025/01/10/vllm-2024-wrapped-2025-vision.html">vLLM 2024 Retrospective and 2025 Vision | vLLM 博客（英文版）</a>。与专注于有限模型系列的专业解决方案相比，这种全面的支持具有根本性的优势。</p>
<h3 id="不只是传统-llm">不只是传统 LLM</h3>
<p>vLLM 的架构支持更多样的模型类型：</p>
<h4 id="语言模型">语言模型</h4>
<ul>
<li>Llama 系列。</li>
<li>Mistral 和 Mixtral 多模态模型。</li>
<li>Google 的 Gemma 模型。</li>
<li>IBM 的 Granite 系列。</li>
<li>阿里巴巴的 Qwen 模型。</li>
</ul>
<h4 id="多模态模型">多模态模型</h4>
<ul>
<li>视觉-语言模型（LLaVA, Qwen-VL）。</li>
<li>文档理解模型。</li>
<li>音频-语言模型。</li>
<li>视频理解能力。</li>
</ul>
<h4 id="专用架构">专用架构</h4>
<ul>
<li>状态空间语言模型的生产支持，探索非 Transformer 语言模型的未来。<a href="https://blog.vllm.ai/2025/01/10/vllm-2024-wrapped-2025-vision.html">vLLM 2024 Retrospective and 2025 Vision | vLLM 博客（英文版）</a>。</li>
<li>混合专家（MoE）模型。</li>
<li>编码器-解码器架构。</li>
<li>嵌入和重排序模型。</li>
</ul>
<hr>
<h2 id="模型集成简易流程">模型集成简易流程</h2>
<p>将新模型加入 vLLM 通常包括以下步骤：</p>
<ol>
<li><strong>模型架构定义</strong>：使用熟悉的 PyTorch 模式实现。</li>
<li><strong>Attention 后端集成</strong>：利用现有的优化内核。</li>
<li><strong>Tokenizer 支持</strong>：兼容 Direct Hugging Face。</li>
<li><strong>配置映射</strong>：基于标准 YAML 的配置。</li>
</ol>
<p>这样的标准化流程意味着组织能更快将新模型投入生产。例如，在 2025 年 8 月 GPT-OSS 发布后不久，vLLM 的 <a href="https://github.com/vllm-project/vllm/releases/tag/v0.11.0">v0.11.0 版本</a>就加入了对其生产部署的支持。</p>
<hr>
<h2 id="与-hugging-face-生态整合">与 Hugging Face 生态整合</h2>
<p>vLLM 与 Hugging Face 的原生兼容带来了便捷性：</p>
<h3 id="直接模型加载">直接模型加载</h3>
<ul>
<li>直接从 Hugging Face Hub 和/或兼容 S3 的对象存储后端（例如 MinIO）加载模型。</li>
<li>支持私有模型库。</li>
<li>自动权重转换处理。</li>
</ul>
<h3 id="分词器兼容性">分词器兼容性</h3>
<ul>
<li>使用现有的分词器实现。</li>
<li>支持自定义分词器。</li>
<li>快速分词器优化</li>
</ul>
<h3 id="配置保存">配置保存</h3>
<ul>
<li>尊重特定于模型的配置。</li>
<li>支持自定义模型参数。</li>
<li>兼容经过微调的变体。</li>
</ul>
<hr>
<h2 id="生产级模型管理">生产级模型管理</h2>
<p>在一些企业级产品和方案中增加了模型生命周期的企业功能：</p>
<h3 id="模型注册集成">模型注册集成</h3>
<ul>
<li>已部署模型的版本控制。</li>
<li>A/B 测试功能。</li>
<li>回滚机制。</li>
</ul>
<h3 id="模型监控">模型监控</h3>
<ul>
<li>每个模型的性能跟踪。</li>
<li>使用情况分析和成本归因。</li>
<li>偏差检测功能。</li>
</ul>
<hr>
<h2 id="面向未来的模型支持">面向未来的模型支持</h2>
<p>模型创新速度极快，推理平台必须具备适应能力：</p>
<h3 id="社区贡献">社区贡献</h3>
<ul>
<li>活跃的社区不断添加新模型。</li>
<li>供应商中立的开发流程。</li>
<li>快速集成突破性架构。</li>
</ul>
<h3 id="灵活的架构">灵活的架构</h3>
<ul>
<li>模块化设计支持新的模型范式。</li>
<li>不受特定模型假设的约束。</li>
<li>为后转型架构做好准备。</li>
</ul>
<h3 id="企业验证">企业验证</h3>
<ul>
<li>企业的测试和认证流程。</li>
<li>模型工件的安全扫描。</li>
<li>跨硬件性能验证。</li>
</ul>
<p>这种全面的模型支持确保组织能够在新模型出现时立即采用它们，而无需进行平台迁移或架构更改，这是人工智能领域持续快速发展的一项关键能力。</p>
<hr>
<h2 id="使用企业级产品进行企业部署">使用企业级产品进行企业部署</h2>
<h3 id="从第一天起就是生产级">从第一天起就是生产级</h3>
<p>大多数企业级产品提供了一个灵活、可扩展的 MLOps 平台，提供构建、部署和管理 AI 应用的工具。它采用开源技术构建，为团队提供值得信赖且操作一致的功能，用于实验、模型服务和交付创新应用。了解更多关于企业级产品如何应对这一挑战的信息。</p>
<h3 id="与-kserve-的整合智能模型服务">与 KServe 的整合：智能模型服务</h3>
<p>在许多产品和方案中对 vLLM 与 KServe 的集成提供了企业级服务功能：</p>
<h4 id="genai-功能">GenAI 功能</h4>
<ul>
<li>使用 vLLM 服务运行时进行多节点/多 GPU 推理。</li>
<li>通过 vLLM + LMCache 集成卸载键值缓存。</li>
<li>通过模型缓存高效重用模型。</li>
<li>集成 KEDA，允许基于外部指标自动扩缩。</li>
<li>通过与 Envoy AI Gateway 集成实现速率限制和请求路由。</li>
<li>通过 LLMInferenceService CRD 访问 llm-d 功能。</li>
</ul>
<h4 id="高级自动扩缩">高级自动扩缩</h4>
<ul>
<li>基于请求的扩缩，实现资源利用率优化。</li>
<li>缩减​​至零功能，实现成本优化。</li>
<li>基于流量模式的预测性扩缩。</li>
<li>多指标扩缩（GPU 利用率、队列深度、延迟）。</li>
</ul>
<h4 id="流量管理">流量管理</h4>
<ul>
<li>金丝雀部署，确保模型更新安全。</li>
<li>蓝绿部署，实现即时回滚。</li>
<li>A/B 测试，用于模型比较。</li>
<li>影子流量，用于验证。</li>
</ul>
<h4 id="服务网格集成">服务网格集成</h4>
<ul>
<li>使用 Istio 进行端到端加密。</li>
<li>高级路由和负载均衡。</li>
<li>熔断和重试逻辑。</li>
<li>用于调试的分布式跟踪。</li>
</ul>
<h4 id="自动化运维">自动化运维</h4>
<ul>
<li>健康检查和自动恢复。</li>
<li>资源优化建议。</li>
<li>自动化证书管理。</li>
<li>日志聚合和分析。</li>
</ul>
<h3 id="安全与合规">安全与合规</h3>
<p>在企业部署中，必须重视安全：</p>
<h4 id="访问控制">访问控制</h4>
<ul>
<li>RBAC 与企业身份提供商集成。</li>
<li>模型级访问权限。</li>
<li>API 密钥管理。</li>
<li>所有操作的审计日志记录。</li>
</ul>
<h4 id="数据保护">数据保护</h4>
<ul>
<li>静态和传输中加密。</li>
<li>私有端点选项。</li>
<li>网络策略实施。</li>
<li>合规性报告工具。</li>
</ul>
<h4 id="供应链安全">供应链安全</h4>
<ul>
<li>签名容器镜像。</li>
<li>SBOM（软件物料清单）生成。</li>
<li>漏洞扫描。</li>
<li>基于策略的部署控制。</li>
</ul>
<h3 id="mlperf-验证的性能">MLPerf 验证的性能</h3>
<p>一些企业级解决方案和产品与众多知名服务器原始设备制造商（OEM）合作，使用搭载 NVIDIA GPU 和 vLLM 推理运行时的产品发布了令人印象深刻的 MLPerf 推理结果，在应对这一挑战方面取得了重大进展。加速生成式 AI 的采用，这些结果验证了：</p>
<ul>
<li>规模化生产级性能。</li>
<li>高效的资源利用率。</li>
<li>负载下一致的延迟。</li>
<li>多实例协调能力。</li>
</ul>
<h3 id="集成可观测性">集成可观测性</h3>
<p>无需额外工具即可进行全面监控：</p>
<h4 id="指标和仪表板">指标和仪表板</h4>
<ul>
<li>预建 Grafana 仪表板，用于 vLLM 指标。</li>
<li>预建 Grafana 仪表板，用于驱动路由决策的请求调度程序指标。</li>
<li>令牌生成率和延迟。</li>
<li>GPU 利用率和内存使用率。</li>
<li>队列深度和拒绝率。</li>
</ul>
<h4 id="警报和响应">警报和响应</h4>
<ul>
<li>SLA 违规自动警报。</li>
<li>与企业监控系统集成。</li>
<li>Runbook 自动化功能。</li>
<li>容量规划洞察。</li>
</ul>
<h3 id="成本管理与优化未来交付">成本管理与优化（未来交付）</h3>
<p>专为企业成本控制设计的功能：</p>
<ul>
<li><strong>计费/显示</strong>：按团队或项目跟踪使用情况。</li>
<li><strong>资源配额</strong>：防止成本失控。</li>
<li><strong>竞价型实例支持</strong>：降低批量工作负载的成本。</li>
<li><strong>空闲检测</strong>：自动缩减未使用的资源。</li>
</ul>
<p>这个企业级平台将 vLLM 从高性能推理引擎转变为完整的生产解决方案，可用于关键任务部署。</p>
<hr>
<h2 id="特性对比vllm-vs-tgi-vs-tensorrt-llm">特性对比：vLLM vs TGI vs TensorRT-LLM</h2>
<p>✓ = 全面支持，◐ = 部分/有限支持，✗ = 不支持</p>
<p><img alt="vLLM vs TGI vs TensorRT-LLM" loading="lazy" src="/zh/blog/2025/the-best-ai-inference-vllm/4-vllm-tgi-tensorrt-llm.png"></p>
<hr>
<h2 id="企业级产品与开源-vllm-的主要区别">企业级产品与开源 vLLM 的主要区别</h2>
<ul>
<li>硬件灵活性：最广泛的加速器支持，包括 AMD、Intel、Google TPU 和 CPU。</li>
<li>模型生态系统：支持超过 100 种模型架构，而其他替代方案仅有 25-40 种。</li>
<li>分布式架构：即将推出的 llm-d 支持分解式预填充/解码和系统级 kv-cache 路由，以实现分布式最优扩展。</li>
<li>企业集成：企业级产品集成 KServe 自动扩展。</li>
<li>内存效率：先进的 PagedAttention 和 KV-Cache 管理。</li>
<li>开放开发：PyTorch 基金会项目，社区创新迅速。</li>
</ul>
<hr>
<h2 id="如何作出正确选择">如何作出正确选择</h2>
<p>文中给出一个简单的决策框架：对每个判断点打 “Yes”：</p>
<ul>
<li><strong>0–2 个 “Yes”</strong>：可考虑专用方案（若其满足你的特定需求）</li>
<li><strong>3–5 个 “Yes”</strong>：vLLM 在你的场景中将提供显著优势</li>
<li><strong>6+ 个 “Yes”</strong>：基于 vLLM 的企业级产品是战略性选择</li>
</ul>
<p>每个决策点都涉及基本的架构约束：</p>
<ul>
<li><strong>灵活性要求</strong>决定您是否需要与硬件无关的解决方案。</li>
<li><strong>运营复杂性</strong>评估简化的运营是否值得采用开源技术。</li>
<li><strong>模型与创新</strong>评估快速发展是否需要一个适应性强的平台。</li>
<li><strong>长期可持续性</strong>考虑总体拥有成本和战略风险。</li>
</ul>
<hr>
<h2 id="战略推荐">战略推荐</h2>
<p>如果你对多个维度都倾向 “Yes”，那 vLLM + 企业级产品将提供：</p>
<ul>
<li><strong>最大的灵活性</strong>：可在任何受支持的硬件上部署。</li>
<li><strong>优秀的运维体验</strong>：企业级平台，复杂性极低。</li>
<li><strong>面向未来的能力</strong>：支支持新兴模型和架构。</li>
<li><strong>成本优化</strong>：为每种工作负载选择最佳硬件。</li>
<li><strong>风险控制</strong>：开源基础，提供企业级支持。</li>
</ul>
<p>这些因素的融合使得 vLLM 成为构建可持续人工智能基础设施的组织的战略选择。</p>
<hr>
<h2 id="采纳步骤建议">采纳步骤建议</h2>
<h3 id="立即阶段">立即阶段</h3>
<ul>
<li>在开发环境部署 vLLM + 企业级产品</li>
<li>用自己的模型和工作负载进行测试</li>
<li>验证硬件灵活性与性能</li>
</ul>
<h3 id="短期阶段">短期阶段</h3>
<ul>
<li>实施 KServe 的自动扩缩能力</li>
<li>建立监控与可观察性体系</li>
<li>培训团队操作流程</li>
</ul>
<h3 id="中期阶段">中期阶段</h3>
<ul>
<li>设计生产部署架构</li>
<li>若需要，实施混合云模式</li>
<li>为 llm-d 分布式架构做准备</li>
</ul>
<h3 id="长期阶段">长期阶段</h3>
<ul>
<li>优化成本（选硬件、资源调度）</li>
<li>引入更高级部署模式</li>
<li>将自己的改进贡献给社区</li>
</ul>
<hr>
<h2 id="总结">总结</h2>
<p>选择 LLM 推理平台代表着一项战略承诺，它将影响您组织未来几年的人工智能能力。</p>
<p>我们的分析表明，企业级产品上的 vLLM 独特地满足了企业 LLM 部署的三个关键要求：</p>
<ol>
<li><strong>灵活性</strong>：可在任意硬件与混合云环境部署</li>
<li><strong>可扩展性</strong>：先进内存管理与 llm-d 架构使资源利用效率提升 10–100 倍</li>
<li><strong>可持续性</strong>：开源基础 + 企业支持，避免陷入锁定</li>
</ol>

<blockquote>
  <p>虽然 TensorRT-LLM 提供了 NVIDIA 专属优化，TGI 也提供了 Hugging Face 集成，但“只有 vLLM 能够提供快速发展的 AI 领域所需的架构灵活性”。
凭借对 100 多种模型架构的支持、与硬件无关的设计以及 PyTorch 和 Linux 基金会的支持，-&gt;“搭载 vLLM 的企业级产品为长期成功奠定了最坚实的基础”。</p>

</blockquote>
]]></content:encoded></item><item><title>玩转大语言模型：基于 vLLM 框架的大模型推理优化实现参数 / 数据（P/D）分离</title><link>https://wilsonwu.me/zh/blog/2025/vllm-pd-separation/</link><pubDate>Mon, 29 Sep 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/vllm-pd-separation/</guid><description>随着大语言模型规模的不断增长，推理部署面临显存紧张与并发受限的挑战。本文介绍如何借助 vLLM 框架，通过 参数（Parameter）/ 数据（Data）分离（P/D Separation）优化推理性能。我们将从原理、架构思路到配置与代码实例逐步展开，展示如何让参数长期驻留在专用 GPU，而输入序列与 KV Cache 等动态数据独立管理，从而提升吞吐率、降低成本并增强系统扩展性。</description><content:encoded><![CDATA[<p>大模型在推理部署时，往往存在<strong>显存瓶颈</strong>：</p>
<ul>
<li>模型参数（Parameters, P）动辄上百 GB，需要长期驻留显存。</li>
<li>输入/输出数据（Data, D）则随请求动态变化，但往往和参数耦合在同一设备上，导致显存占用不均衡，扩展性受限。</li>
</ul>
<p>为了解决这一问题，可以借助 <strong>vLLM 框架</strong>实现<strong>参数 / 数据（P/D）分离</strong>，提升推理系统的灵活性和吞吐。</p>
<h2 id="大模型推理的资源瓶颈">大模型推理的资源瓶颈</h2>
<p>以一个 70B 规模的 Transformer 模型为例：</p>
<ul>
<li>参数权重（FP16 存储）约需 <strong>140GB 显存</strong>；</li>
<li>每次请求输入的序列数据、KV Cache 会消耗额外显存，并随 batch size 增长而急剧膨胀。</li>
</ul>
<p>如果不加区分地将 P 与 D 放在同一块 GPU：</p>
<ol>
<li>参数长期驻留，挤压了用于动态数据的显存；</li>
<li>多实例并发时，数据显存不足，限制了吞吐。</li>
</ol>
<p>因此，在分布式推理系统中，业界逐渐采用 <strong>参数与数据分离（P/D Separation）</strong> 的架构思路。</p>
<h2 id="vllm-简介">vLLM 简介</h2>
<p><a href="https://vllm.ai/">vLLM</a> 是一个高性能的大模型推理引擎，核心优势包括：</p>
<ul>
<li><strong>PagedAttention</strong>：高效管理 KV Cache，支持大批量并发；</li>
<li><strong>高吞吐率</strong>：相较于 Hugging Face Transformers 推理，吞吐提升数倍；</li>
<li><strong>灵活的分布式支持</strong>：可结合 DeepSpeed、Megatron 等方案，支持参数/数据分布式存储与调度。</li>
</ul>
<p>vLLM 的模块化架构，使其天然适合实现 <strong>P/D 分离</strong>。</p>
<h2 id="pd-分离的实现思路">P/D 分离的实现思路</h2>
<p>在 vLLM 中，推理流程大致分为两个部分：</p>
<ol>
<li>
<p><strong>参数侧（P）</strong></p>
<ul>
<li>模型权重加载与存放；</li>
<li>可通过 <strong>ZeRO-3 / Tensor Parallel</strong> 等策略将参数分布在多 GPU 节点上；</li>
<li>参数在整个推理生命周期中保持常驻，不随请求波动。</li>
</ul>
</li>
<li>
<p><strong>数据侧（D）</strong></p>
<ul>
<li>动态输入序列、KV Cache、采样状态等；</li>
<li>可使用 <strong>PagedAttention</strong> 管理显存分页；</li>
<li>支持把部分 KV Cache 放到 CPU/Host 内存，再动态迁移到 GPU。</li>
</ul>
</li>
</ol>
<h2 id="核心机制">核心机制</h2>
<p>其核心机制是显存资源隔离：</p>
<ul>
<li><strong>权重（P）</strong> 固定驻留在专门的 GPU（参数节点）；</li>
<li><strong>数据（D）</strong> 可以按需分配到其他 GPU（数据节点），甚至利用 CPU 内存 / NVMe。</li>
</ul>
<p>这样：</p>
<ul>
<li>参数显存不与动态数据抢占空间；</li>
<li>数据可以根据请求动态扩展，提升并发能力。</li>
</ul>
<h2 id="在-vllm-中的实现路径">在 vLLM 中的实现路径</h2>
<p>实现 P/D 分离的常见方法：</p>
<ol>
<li>
<p><strong>参数分片（Parameter Sharding）</strong></p>
<ul>
<li>借助 vLLM 对 Megatron-LM / DeepSpeed 的支持，将模型参数按张量维度切分到多 GPU；</li>
<li>仅在推理计算时聚合，不需要在每个设备上完整存放参数。</li>
</ul>
</li>
<li>
<p><strong>KV Cache 分离（Data Sharding）</strong></p>
<ul>
<li>通过 vLLM 的 <strong>PagedAttention</strong>，可以把 KV Cache 存在独立的 GPU 或 Host 内存；</li>
<li>在推理过程中按需调页，类似操作系统的虚拟内存机制。</li>
</ul>
</li>
<li>
<p><strong>Pipeline 并行化</strong></p>
<ul>
<li>将模型计算划分为不同阶段，前几层专门占用参数节点，后几层在数据节点完成；</li>
<li>在流水线中，数据逐层传递，而参数保持分片驻留。</li>
</ul>
</li>
</ol>
<h2 id="基于-vllm-的-pd-分离实战">基于 vLLM 的 P/D 分离实战</h2>
<p>下面给出两部分内容：</p>
<ul>
<li><strong>配置示例</strong>：如何在多 GPU 环境下指定参数和数据分布；</li>
<li><strong>代码示例</strong>：如何加载模型并运行推理，展示参数常驻、数据动态分配。</li>
</ul>
<h2 id="配置示例">配置示例</h2>
<p>假设我们有 4 张 GPU：</p>
<ul>
<li><strong>GPU 0,1</strong>：专门存放模型参数（P）；</li>
<li><strong>GPU 2,3</strong>：主要承载 KV Cache 等动态数据（D）。</li>
</ul>
<p>我们可以在启动 vLLM 时设置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>CUDA_VISIBLE_DEVICES<span style="color:#f92672">=</span>0,1,2,3 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>python -m vllm.entrypoints.api_server <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --model /path/to/llama-2-70b <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --tensor-parallel-size <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --gpu-memory-utilization 0.85 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --enable-chunked-prefill <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --enforce-eager <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --kv-cache-dtype fp16 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    --max-num-batched-tokens <span style="color:#ae81ff">4096</span>
</span></span></code></pre></div><p>这里的关键点：</p>
<ul>
<li><code>--tensor-parallel-size 2</code>：只在 GPU 0,1 上做参数分片存放；</li>
<li><code>--gpu-memory-utilization</code>：控制参数占用的比例，留出余量给数据；</li>
<li><code>--kv-cache-dtype fp16</code>：降低 KV Cache 精度，减少数据显存占用；</li>
<li><strong>vLLM 内部的 PagedAttention</strong> 会自动把 KV Cache 分配到 GPU 2,3，如果溢出则回退到 CPU 内存。</li>
</ul>
<h2 id="代码示例">代码示例</h2>
<p>下面是一个 Python 调用示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> vllm <span style="color:#f92672">import</span> LLM, SamplingParams
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -------- 参数（P）分离 --------</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># tensor_parallel_size=2 表示参数分布在前两张 GPU（0,1）</span>
</span></span><span style="display:flex;"><span>llm <span style="color:#f92672">=</span> LLM(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/path/to/llama-2-70b&#34;</span>,
</span></span><span style="display:flex;"><span>    tensor_parallel_size<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>    gpu_memory_utilization<span style="color:#f92672">=</span><span style="color:#ae81ff">0.85</span>,
</span></span><span style="display:flex;"><span>    enforce_eager<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,       <span style="color:#75715e"># 避免显存碎片</span>
</span></span><span style="display:flex;"><span>    enable_chunked_prefill<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    kv_cache_dtype<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;fp16&#34;</span>     <span style="color:#75715e"># 数据（D）使用更轻量的缓存精度</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -------- 数据（D）分离 --------</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># PagedAttention 会自动管理 KV Cache 的存放位置（GPU2, 3, 甚至 CPU 内存）</span>
</span></span><span style="display:flex;"><span>sampling_params <span style="color:#f92672">=</span> SamplingParams(
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.8</span>,
</span></span><span style="display:flex;"><span>    top_p<span style="color:#f92672">=</span><span style="color:#ae81ff">0.95</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;解释一下通过 vLLM 实现 P/D 分离的好处。&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 运行推理</span>
</span></span><span style="display:flex;"><span>outputs <span style="color:#f92672">=</span> llm<span style="color:#f92672">.</span>generate([prompt], sampling_params)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;模型输出：&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> output <span style="color:#f92672">in</span> outputs:
</span></span><span style="display:flex;"><span>    print(output<span style="color:#f92672">.</span>outputs[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>text)
</span></span></code></pre></div><h2 id="验证方式">验证方式</h2>
<p>运行时可以用 <code>nvidia-smi</code> 观察：</p>
<ul>
<li>GPU 0,1 显存几乎恒定，主要承载参数；</li>
<li>GPU 2,3 的显存随请求数量/序列长度波动，主要承载 KV Cache；</li>
<li>如果输入过大，KV Cache 会溢出到 CPU 内存，但仍可继续推理。</li>
</ul>
<h2 id="实际效果与收益">实际效果与收益</h2>
<p>通过 P/D 分离，可以得到以下好处：</p>
<ul>
<li><strong>提升 GPU 利用率</strong>：参数和数据分工明确，避免显存冲突；</li>
<li><strong>支持更大 batch size</strong>：数据显存可扩展，吞吐率提升显著；</li>
<li><strong>降低成本</strong>：参数可驻留在高端 GPU，而数据可放到中低端 GPU 或 CPU 内存，形成分层存储；</li>
<li><strong>灵活性</strong>：适配多种硬件资源组合（A100 + H100, GPU + CPU, GPU + NVMe）。</li>
</ul>
<p>在实践中，结合 vLLM 的 PagedAttention，P/D 分离的效果尤为明显，可让大模型推理在同样硬件下实现 <strong>数倍并发提升</strong>。</p>
<h2 id="总结">总结</h2>
<p>vLLM 框架凭借其高效的 KV Cache 管理和分布式能力，为 <strong>大模型推理的 P/D 分离</strong> 提供了坚实基础。
通过将 <strong>参数（P）</strong> 和 <strong>数据（D）</strong> 解耦，可以更好地利用硬件资源，提升推理性能与扩展性。</p>
<p>未来，随着 <strong>混合精度存储、分层缓存（GPU+CPU+NVMe）</strong> 的发展，P/D 分离架构将逐渐成为大模型推理的主流范式。</p>
]]></content:encoded></item><item><title>玩转大语言模型：微软最新开源长语音大模型 VibeVoice 入门</title><link>https://wilsonwu.me/zh/blog/2025/microsoft-vibevoice/</link><pubDate>Thu, 18 Sep 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/microsoft-vibevoice/</guid><description>本文介绍使用微软最新开源的 VibeVoice 大模型，在本地进行推理实现长文本，多角色文字转语音。</description><content:encoded><![CDATA[<h2 id="什么是-vibevoice">什么是 VibeVoice？</h2>
<p><strong><a href="https://microsoft.github.io/VibeVoice/">VibeVoice</a></strong> 是 Microsoft Research 发布的一套面向<strong>长篇、多说话人、对话式</strong>语音合成的研究框架，目标场景例如整集播客、有声剧或访谈：能在单次生成中维持说话人一致性并处理自然的换手（turn-taking）。模型家族包含多个规模（例如 1.5B、7B 等），并在 Hugging Face 上以 <a href="https://huggingface.co/microsoft/VibeVoice-1.5B"><code>microsoft/VibeVoice-1.5B</code></a> 形式开放（模型卡、模型文件、model card 中有安装/使用与责任使用说明）。</p>
<p>它解决了传统 TTS（Text-To-Speech）系统的一些痛点，比如：</p>
<ul>
<li>难以维持<strong>长时间</strong>对话的语音一致性（speaker consistency）；</li>
<li>多说话人的切换自然性（turn-taking）差；</li>
<li>效率低 — 长文本 + 多说话人时，资源消耗大。</li>
</ul>
<h2 id="核心创新与架构">核心创新与架构</h2>
<p>VibeVoice 有几个比较新的或者关键的技术设计：</p>
<table>
	<thead>
			<tr>
					<th>组件</th>
					<th>功能 / 目的</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Continuous Speech Tokenizers</strong>（声学 + 语义两种）</td>
					<td>用来把音频压缩成低帧率（7.5 Hz）表示，同时保留语义与音质信息。声学 token 与语义 token 分别负责声音细节和内容表达。</td>
			</tr>
			<tr>
					<td><strong>LLM 基础模型</strong>（Large Language Model）</td>
					<td>在该版本里用的是 Qwen2.5-1.5B，用来处理文本、说话人信息以及上下文对话流。</td>
			</tr>
			<tr>
					<td><strong>Diffusion Head</strong></td>
					<td>对声学 VAE 的特征进行预测，是生成高保真声音细节的模块。这个模块较轻 (大致 4 层结构)，在推理阶段使用 diffusion 的技术（包括去噪等）。</td>
			</tr>
			<tr>
					<td><strong>上下文长度 &amp; 多说话人</strong></td>
					<td>支持高达 <strong>90 分钟</strong>语音生成，最多 <strong>4 个</strong>说话人。</td>
			</tr>
	</tbody>
</table>
<p>架构图如下：</p>
<p><img alt="VibeVoice 架构" loading="lazy" src="/zh/blog/2025/microsoft-vibevoice/vibevoice-arch.jpg"></p>
<h2 id="优点和局限">优点和局限</h2>
<h3 id="优点">优点</h3>
<ol>
<li><strong>长篇幅对话能力</strong> — 能生成近 90 分钟的连续对话，并维持说话人一致性。</li>
<li><strong>多说话人支持</strong> — 最多支持 4 个不同说话人的切换，且对话流程自然。</li>
<li><strong>压缩效率高</strong> — 用 7.5 Hz 帧率的 speech tokenizer，音频被高度压缩但仍保证质量。</li>
<li><strong>语义 + 声学分离</strong> — 语义 tokenizer（semantic）负责内容，声学 tokenizer（acoustic）负责声音细节，两者协同。</li>
</ol>
<h3 id="局限">局限</h3>
<ol>
<li><strong>语言支持有限</strong> — 目前主要是英语与中文，跨语言能力还在研究中。</li>
<li><strong>背景音乐 / 音效不支持</strong> — 当前专注纯语音合成，没有加入背景音或重叠说话人（speaker overlap）。</li>
<li><strong><a href="https://github.com/microsoft/VibeVoice">仓库暂时关闭</a></strong> — 官方声明因 “部分用途超出初衷 / 不符合责任使用” 的情况，仓库暂时被 disable。意味着可能无法立即拿到源码或全部功能。</li>
<li><strong>硬件要求 / 资源消耗</strong> — 虽然压缩效率高，但生成长篇语音 + 多说话人 + 高保真度仍需要较好的 GPU 资源。</li>
<li><strong>潜在滥用风险</strong> — 用来做语音克隆、假扮、deepfake 等可能的恶意用途。官方在文档中也提到要谨慎。</li>
</ol>
<h2 id="快速上手">快速上手</h2>
<p>下面给出具体实现步骤及上手流程。注意：由于官方仓库目前不包含完整脚本，实际推理通常依赖社区实现或 Hugging Face Spaces / Colab 示例。所列步骤已参考 Hugging Face 模型页与社区示例仓库。</p>
<h3 id="先决条件">先决条件</h3>
<ul>
<li>
<p>Python 3.8+；建议在隔离虚拟环境（venv / conda）中操作。</p>
</li>
<li>
<p>GPU：带 CUDA 的 NVIDIA GPU（具体 VRAM 与性能见第 5 节）。若使用 Apple Silicon / macOS，可参见社区讨论与专门脚本。</p>
</li>
<li>
<p>安装工具（示例）：</p>
<ul>
<li><code>pip install huggingface_hub transformers accelerate torch soundfile torchaudio numpy einops</code></li>
<li>本地需要 <code>ffmpeg</code>（用于音频切分/拼接与格式转换）。</li>
</ul>

<blockquote>
  <p>具体版本请按 PyTorch 官网或社区脚本说明选择（不同 CUDA 版本的 wheel 不同）。参考社区仓库的 requirements 列表以确保兼容。</p>

</blockquote>
</li>
</ul>
<h3 id="下载模型权重">下载模型权重</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> huggingface_hub <span style="color:#f92672">import</span> snapshot_download
</span></span><span style="display:flex;"><span>model_dir <span style="color:#f92672">=</span> snapshot_download(<span style="color:#e6db74">&#34;microsoft/VibeVoice-1.5B&#34;</span>)
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;模型已下载到&#34;</span>, model_dir)
</span></span></code></pre></div><p>注意：<code>snapshot_download</code> 会把 HF 模型文件和模型卡同步拉取到本地；若模型文件较大，请确保磁盘与网络条件。</p>
<h3 id="使用社区推理脚本">使用社区推理脚本</h3>
<p>很多社区仓库或 Hugging Face Spaces 提供了 <code>demo/inference_from_file.py</code> 之类的脚本。示例命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 在 Colab / 本地环境中操作</span>
</span></span><span style="display:flex;"><span>git clone https://huggingface.co/technicalheist/vibevoice-1.5b
</span></span><span style="display:flex;"><span>cd vibevoice-1.5b
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 安装 Python 依赖（按模型页 “Requirements” 部分）</span>
</span></span><span style="display:flex;"><span>pip install torch transformers soundfile ffmpeg-python  <span style="color:#75715e"># plus 其他必要包</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 创建一个 transcript 文件，比如 transcript.txt</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Speaker 1: Hello, how are you doing?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Speaker 2: I&#39;m good, thanks. What about you?&#34;</span> &gt; transcript.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 用 demo 脚本生成语音</span>
</span></span><span style="display:flex;"><span>python demo/inference_from_file.py <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model_path /path/to/technicalheist/vibevoice-1.5b <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --txt_path transcript.txt <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --speaker_names <span style="color:#e6db74">&#34;Alice,Bob&#34;</span>
</span></span></code></pre></div><p>社区实现（例如 <code>technicalheist/vibevoice-1.5b</code>、<code>shijincai/VibeVoice</code> 等）提供了可运行的示例与依赖说明；使用前请仔细阅读其 README 并依据其中的依赖和指引来配置环境。示例脚本名与参数在不同实现中可能不同，请以具体仓库中的文档为准。</p>
<h3 id="简单示例">简单示例</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>Speaker 1: Hello, welcome to our podcast.
</span></span><span style="display:flex;"><span>Speaker 2: Thanks — happy to be here. Let&#39;s talk about VibeVoice.
</span></span><span style="display:flex;"><span>Speaker 1: Sure, start with the key idea...
</span></span></code></pre></div><p>将上面保存为 <code>transcript.txt</code> 并传入 <code>--txt_path</code> 即可（脚本会解析说话人标记并输出对应音轨）。社区脚本通常会把每个说话人的音色/说话风格作为参数或示例内置。</p>
<h2 id="结论">结论</h2>
<p>微软的 VibeVoice 是当前 TTS / 语音合成领域一个非常令人兴奋的进展。它在<strong>长篇幅语音 + 多说话人</strong>上取得了突破，尤其是在对话流畅性和说话人一致性方面，技术设计（如低帧率的双 tokenizers + next-token diffusion + LLM 架构）也很前沿。</p>
<p>如果你正在做播客、有声书或者对话剧本的语音合成，这可能是一个非常有用的工具／研究基础。不过在商业部署或者包含敏感语音身份／法律边界用途时，需要格外注意责任使用，以及是否有完整的许可与风险评估。</p>
]]></content:encoded></item><item><title>玩转大语言模型：使用 SGLang 框架实现大语言模型推理入门教程</title><link>https://wilsonwu.me/zh/blog/2025/getting-started-with-sglang/</link><pubDate>Thu, 10 Jul 2025 00:00:00 -0400</pubDate><guid>https://wilsonwu.me/zh/blog/2025/getting-started-with-sglang/</guid><description>这篇文章带你从零开始，快速上手 SGLang：了解它是什么、为什么快、如何安装与启动、如何用 OpenAI 兼容接口调用，并通过一个完整的 Demo（含流式输出与并发调用）一步步跑通。文章面向刚接触 SGLang 的同学，几分钟内即可看到本地模型在线响应。</description><content:encoded><![CDATA[<p>随着大语言模型热度的升级，企业和个人使用者的研究重心逐步从训练转移至推理（说白了是由造轮子转变为务实的使用）。而在模型推理领域，最炙手可热的两大框架无疑是 vLLM 和 SGLang，而作为后起之秀的 SGLang，其表现也值得大家关注，今天就基于 SGLang 为大家带来一篇入门教程文章，希望能帮助更多希望了解大语言模型及 SGLang 框架的朋友。</p>
<h2 id="sglang-简介">SGLang 简介</h2>
<p>SGLang 是一款面向大语言模型（LLM）和视觉语言模型（VLM）的高性能推理框架，通过精心设计的后端运行时与前端语言协同工作，使模型交互更加高效且可控。其核心优势包括：</p>
<ul>
<li><strong>高效后端运行时</strong>：采用创新的 RadixAttention 技术实现前缀缓存，支持跳跃式受限解码、零开销 CPU 调度、连续批处理、令牌注意力（分页注意力）、张量并行、FlashInfer 内核、分块预填充以及多种量化技术（FP8/INT4/AWQ/GPTQ），显著提升推理效率。</li>
<li><strong>灵活前端语言</strong>：提供直观且强大的 LLM 编程接口，支持链式生成调用、高级提示工程、复杂控制流、多模态输入、并行处理及外部系统交互。</li>
<li><strong>广泛模型兼容性</strong>：支持多种主流生成式模型（Llama、Gemma、Mistral、QWen、DeepSeek、LLaVA 等）、嵌入模型（e5-mistral、gte）及奖励模型（Skywork），并提供简便的新模型扩展机制。</li>
<li><strong>活跃开源生态</strong>：拥有蓬勃发展的社区支持，已获得广泛业界认可（截至 2025 年 3 月 17 日，GitHub 累计星标超过 12,000）。</li>
</ul>
<p>其技术架构如下：</p>
<p><img alt="SGLang 架构" loading="lazy" src="/zh/blog/2025/getting-started-with-sglang/1-sglang-architecture.png"></p>
<p>除此之外，对于初学者，需要了解其以下特性：</p>
<ul>
<li><strong>OpenAI 兼容 API</strong>：直接用 openai Python SDK 或 cURL 调用，不用改你的上层业务代码。</li>
<li><strong>高吞吐与低延迟</strong>：结合连续批处理与前缀缓存等技巧，让相同前缀的请求复用计算。</li>
<li><strong>生产友好</strong>：支持多并发、流式输出、可与 Hugging Face 模型库直接对接。</li>
</ul>
<h2 id="环境准备">环境准备</h2>
<p>工欲善其事必先利其器，要完成本文的新手实验，需准备如下环境：</p>
<ul>
<li>操作系统：建议 Linux（常用为 Ubuntu 20.04+）。WSL2 也可尝试。</li>
<li>Python：建议 3.10 或 3.11。</li>
<li>GPU：建议 NVIDIA 显卡，24GB 显存可跑 7B/8B 级（如 Llama 3.1 8B）。没有 GPU 也能跑小模型或量化模型，但性能有限。</li>
<li>模型来源：Hugging Face（如 meta-llama/Llama-3.1-8B-Instruct）或本地路径。</li>
</ul>
<h2 id="安装-sglang">安装 SGLang</h2>
<p>环境就绪后，使用 pip 安装 SGLang（确保已安装 pip、可选创建虚拟环境）。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>pip install sglang openai huggingface<span style="color:#f92672">-</span>hub
</span></span></code></pre></div><p>如果你需要从 Hugging Face 自动下载私有或受限模型，请先登录或设置 Token：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>huggingface-cli login
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 或</span>
</span></span><span style="display:flex;"><span>export HF_TOKEN<span style="color:#f92672">=</span>您的Token
</span></span></code></pre></div><h2 id="启动-sglang-服务">启动 SGLang 服务</h2>
<p>可以通过以下两种启动方式进行 SGLang 服务的启动。不同版本的 SGLang 命令行参数可能略有差异，如果出现参数名称不匹配，先执行 &ndash;help 查看本机版本支持的参数。</p>
<h3 id="通过-launcher-启动">通过 launcher 启动</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python -m sglang.launcher <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --model meta-llama/Llama-3.1-8B-Instruct <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --host 0.0.0.0 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --port <span style="color:#ae81ff">30000</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --tp <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --max-model-len <span style="color:#ae81ff">8192</span>
</span></span></code></pre></div><p>其中常用参数说明如下：</p>
<ul>
<li>&ndash;model：Hugging Face 模型名称或本地模型权重路径</li>
<li>&ndash;host/&ndash;port：服务监听地址与端口</li>
<li>&ndash;tp：Tensor Parallel 并行度（单卡设为 1；多卡可根据需要设为 2、4 等）</li>
<li>&ndash;max-model-len：上下文最大长度</li>
</ul>
<p>另外需要注意以下两点：</p>
<ul>
<li>如果使用的是 Hugging Face 模型，首次运行会下载模型权重，请确保网络通畅。</li>
<li>如果显存吃紧，可优先尝试更小模型，或减少 &ndash;max-model-len。</li>
</ul>
<h3 id="通过-sglang-命令启动">通过 sglang 命令启动</h3>
<p>如果您使用的版本提供 sglang serve 命令，也可使用如下命令进行启动：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sglang serve -m meta-llama/Llama-3.1-8B-Instruct --host 0.0.0.0 --port <span style="color:#ae81ff">30000</span>
</span></span></code></pre></div><h2 id="快速验证">快速验证</h2>
<p>服务启动后，OpenAI 兼容接口即可使用：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl http://127.0.0.1:30000/v1/chat/completions <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Content-Type: application/json&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#34;Authorization: Bearer EMPTY&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;model&#34;: &#34;meta-llama/Llama-3.1-8B-Instruct&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;messages&#34;: [
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: &#34;You are a helpful assistant.&#34;},
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;用两句话介绍一下你自己。&#34;}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ],
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;max_tokens&#34;: 128,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;temperature&#34;: 0.2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }&#39;</span>
</span></span></code></pre></div><p>如果运行正常，将返回标准的 ChatCompletion JSON 响应。</p>
<h2 id="使用-python-openai-sdk-进行调用">使用 Python OpenAI SDK 进行调用</h2>
<p>非流式调用（最简单的问答）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAI(
</span></span><span style="display:flex;"><span>    base_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://127.0.0.1:30000/v1&#34;</span>,
</span></span><span style="display:flex;"><span>    api_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;EMPTY&#34;</span>  <span style="color:#75715e"># 本地服务通常不校验</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>messages <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;system&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;You are a helpful assistant who answers in Chinese.&#34;</span>},
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;请用三句话介绍帕累托最优。&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>resp <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;meta-llama/Llama-3.1-8B-Instruct&#34;</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>messages,
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.2</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(resp<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>message<span style="color:#f92672">.</span>content)
</span></span></code></pre></div><p>流式输出（边生成边打印）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> openai <span style="color:#f92672">import</span> OpenAI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> OpenAI(base_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://127.0.0.1:30000/v1&#34;</span>, api_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;EMPTY&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>messages <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;system&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;You are a helpful assistant that speaks Chinese.&#34;</span>},
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>: <span style="color:#e6db74">&#34;简述 Transformer 的核心思想，并给出一个现实类比。&#34;</span>}
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stream <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>chat<span style="color:#f92672">.</span>completions<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>    model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;meta-llama/Llama-3.1-8B-Instruct&#34;</span>,
</span></span><span style="display:flex;"><span>    messages<span style="color:#f92672">=</span>messages,
</span></span><span style="display:flex;"><span>    temperature<span style="color:#f92672">=</span><span style="color:#ae81ff">0.2</span>,
</span></span><span style="display:flex;"><span>    max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">300</span>,
</span></span><span style="display:flex;"><span>    stream<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> chunk <span style="color:#f92672">in</span> stream:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> chunk<span style="color:#f92672">.</span>choices <span style="color:#f92672">and</span> chunk<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>delta:
</span></span><span style="display:flex;"><span>        delta <span style="color:#f92672">=</span> chunk<span style="color:#f92672">.</span>choices[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>delta<span style="color:#f92672">.</span>content
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> delta:
</span></span><span style="display:flex;"><span>            print(delta, end<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>, flush<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">[Done]&#34;</span>)
</span></span></code></pre></div><p>通过以上两种方式，我们可以构建自己的推理服务应用，也可以通过市面上开源的软件来接入您部署的推理服务，如 Open-WebUI 等。</p>
<h2 id="常见问题与排错">常见问题与排错</h2>
<ul>
<li>启动时报显存不足（CUDA OOM）
<ul>
<li>尝试更小的模型（如 7B/8B）、减小 <code>--max-model-len</code>、或降低并发。</li>
<li>如果有多卡，增加 &ndash;tp 并用多卡分摊显存。</li>
</ul>
</li>
<li>拉取模型失败或速度慢
<ul>
<li>先执行 <code>huggingface-cli login</code>；或提前手动把模型权重下载到本地路径，并在 <code>--model</code> 里填本地路径。</li>
</ul>
</li>
<li>API 报 <code>Unknown argument</code>
<ul>
<li>不同版本的命令行参数可能有差异。请运行：<code>python -m sglang.launcher --help</code></li>
<li>根据你安装的版本调整参数名，如 <code>--model</code> / <code>--model-path</code>、<code>--tp</code> / <code>--tensor-parallel-size</code> 等。</li>
</ul>
</li>
<li>Python 客户端 <code>401/Key</code> 错误
<ul>
<li>本地服务通常不校验 Key，只要带上一个占位即可（如 <code>EMPTY</code>）。</li>
</ul>
</li>
<li>流式输出不打印
<ul>
<li>确认传了 <code>stream=True</code>；迭代 chunk 时取 <code>choices[0].delta.content</code>。</li>
</ul>
</li>
<li>并发性能不达预期
<ul>
<li>尝试用 Async 客户端并发请求；避免在单线程里串行等待。</li>
<li>检查 GPU 利用率；确认没有 CPU/GPU 数据传输瓶颈。</li>
</ul>
</li>
</ul>
<h2 id="总结">总结</h2>
<p>至此，你已经完成了：</p>
<ul>
<li>安装与启动 SGLang 服务</li>
<li>用 OpenAI 兼容接口进行非流式与流式推理</li>
</ul>
<p>祝你上手顺利，玩得开心！如果遇到命令行参数或 API 的小差异，参考本地版本的 &ndash;help 与官方文档进行快速适配即可。</p>
]]></content:encoded></item></channel></rss>