大模型每秒往外吐多少 token,一直是使用体验里最敏感的那根弦。这一期记录的 Uno,把扩散嫁接到 AR 模型身上,不用 draft model 也敢报出最高 3 倍的加速。

先交代立场:我在 4sapi.com 做多模型路由,推理速度和价格是我给每个模型打标签的两个轴,所以每逢解码架构层面冒出新名字,我都会凑近看看成色,再决定要不要写进这一期的选题单。

一个老问题:token 为什么必须一个一个蹦出来

先从一个常识说起。自回归(AR)模型生成文本的方式,是拿已有的内容当条件,算出下一个 token 的概率分布,采样出一个,拼回序列,再算下一个。第 N 个 token 必须等第 N-1 个落定才有资格开算,这条链路天生串行:显卡的算力再富裕,解码阶段也只能一拍一拍往外挤,算力大部分时间在等上一步的结果。这也是为什么同样的模型,预填充长上下文可以一口气吃进去,往外生成几千 token 却要按秒计时。

串行解码的代价要分两种账法来算。一种按延迟说话:在线聊天、代码补全、agentic 工具调用这类场景,用户感知的是首 token 延迟和端到端等待时长,解码慢一拍,体验就掉一截;工具调用链条里一步慢、步步慢,整套 agent 的节奏都被拖住。另一种按吞吐计费:批量离线任务、数据管道这类场景,单条请求慢一点可以靠堆并发把机器喂满,账面损失有限,真正值钱的是整机的 tokens/s。所以同样是"加速",两种场景的敏感点完全不同:前者盯着首 token 延迟和单请求时延,后者盯着总吞吐。一个新解码架构想两头都站住,并不容易。

两难的旧解法:draft model 背在身上跑

主流的提速手段是投机解码(speculative decoding)。流程大致是:一个独立的小 draft model 先一口气草拟若干个 token,大模型再对这段草稿做一次并行验证,接受连续正确的部分,从第一个错误处退回重算,然后进入下一轮。验证只要一次前向就能批量完成,所以只要草稿命中的比例够高,大模型的一次前向就能结算出多个 token,加速由此而来。

代价也明摆着:得多养一个 draft model。部署上多一份权重、多占一份显存、多一条 KV cache 管理路径;工程上还要处理 draft 与目标模型之间的对齐问题。这里有个绕不开的跷跷板:draft 草得快但错得多,接受率上不去,加速就打折;想草得准就得换更大的 draft,显存占用和验证成本又涨回去。从路由平台的视角看还有一层:要为一个目标模型额外挂一个配套 draft,模型组合一多,这层包袱按对数往上叠,负载均衡的粒度被切得更碎。这就是那个两难——不加 draft,串行慢得难受;加了 draft,系统重得肉疼。

Uno 的答案:参数拆成 AR 权重加轻量扩散权重两组

Uno 给出的答案,是"扩散增强 LLM"。思路是把模型参数解耦成两组:一组 AR 权重,继续用标准 next-token prediction 训练,模型的底子和传统 AR 模型站在同一条能力线上;另一组是新加的轻量扩散权重,用扩散的方式从 AR 模型的分布里并行抽取多个 token。直白地说,底层还是那个熟悉的 AR 模型,扩散部分只负责一件事——让每一步能往前铺好几个 token,而不是挪一个。

这个设计和两条旧路线都划清了界限。和投机解码相比,Uno 不需要独立的 draft model,没有那份额外权重,也没有 draft-verify 那套配套流程;和扩散 LLM(d-LLM)相比,Uno 不牺牲底层 AR 模型的质量——d-LLM 通常要把整个生成范式改成纯扩散式,模型的既往能力相当于推倒重练,Uno 的 AR 底座则原样保留,扩散权重只是外挂的加速层。质量与速度,在这里不再是二选一。

Diffusion Distillation:让扩散权重从 AR 分布里学抽样

那组轻量扩散权重不是凭空长出来的。Uno 的做法是加一个 Diffusion Distillation 阶段,让扩散权重学会从 AR 模型自己的分布里并行抽出多个 token——学的对象就是 AR 权重本身给出的分布,所以抽出来的 token 天然贴着原模型的行为走。更值得记一笔的是开销:因为 AR 权重不动,训练管线里原有的部分照常运转,这个蒸馏阶段对现有 LLM 训练管线的额外开销可以忽略。这不是一个要求推倒重来的新范式,更像往现有流水线末端多接一段工位;工程上的友好程度,直接决定了这类架构能不能真正铺开。

Spec 采样器:固定上下文长度下的无损加速与推理时扩展

采样这一层,Uno 配套提出了一套 Spec 采样器家族。两个关键词值得单独拎出来:一是无损加速,在固定上下文长度的前提下实现,直接回应"快是快了、答案变笨了怎么办"的疑虑——加速不以分布走样为代价;二是推理时扩展,batch 和并行度往上加的时候,收益还能跟着走。成绩单里有一条细节正好对上:相对基座的最高加速,是在设备支持的最大 batch 那一档测到的,说明这套并行不是只在低负载下好看。

三条解码路线的流程对比

把三条解码路线的流程摆在一起,差别一目了然:

【路线一:AR 串行】
  提示词 → [算 token 1] → [算 token 2] → [算 token 3] → [算 token 4] → ...
  每一步都依赖上一步的结果,一拍一个,天生排队。

【路线二:投机解码 draft-verify】
  提示词 → draft model 草拟若干 token → 大模型一次前向并行验证
        → 接受连续正确前缀 / 从错误处退回 → 下一轮继续
  快的前提是多背一个 draft model,且草稿命中率要够高。

【路线三:Uno 扩散并行】
  提示词 → AR 权重给出当前分布 → 扩散权重从该分布并行抽出多个 token
        → 一次前进多步 → 循环
  没有 draft 环节,底层 AR 分布不被替换,并行步长来自额外那组轻量扩散权重。

打个比方:串行是窗口前排队,一单一单办;投机解码是找了个跑腿的先把号取好,窗口再批量核验,跑腿的取错号就得重排;Uno 则是窗口直接一次打出好几张票,而打票的规则是从 AR 分布里学出来的,快而不偏。

和投机解码、d-LLM 各差在哪一步

这一节单独回答两个容易混淆的问题。Uno 和投机解码差在哪?差在不需要独立的 draft model:投机解码的加速依赖另一个模型先草拟再验证,Uno 的并行能力长在自己身上那组扩散权重里,部署上少背一个模型,工程上少一套对齐。Uno 和 d-LLM 差在哪?d-LLM 是把生成范式整个换成扩散,追求并行度的同时,也把底层 AR 模型的质量摆上了祭坛;Uno 反过来,AR 权重原封不动,扩散权重只学"怎么从原分布里并行抽 token",质量曲线和原模型绑在一起。一句话总结:投机解码借外力,d-LLM 换发动机,Uno 给原发动机加了一组副桨。

成绩单:最高 3 倍加速与以小胜大

数字部分照实记。吞吐上,在所有评测 batch size 下,Uno 的吞吐都高于主流投机解码方法——注意是全部 batch size,不是挑出来的几个甜点位;相对基座 AR 模型,最高拿到 3 倍加速,而且这个最高值出现在设备支持的最大 batch 那一档,延迟敏感和吞吐敏感两类场景都有肉吃。规模对比更扎眼:8B 的 Uno 在 agentic 工具调用、编码、长上下文推理这三类基准上,全面超过 26B 的开源扩散模型 DiffusionGemma 和商用的 Mercury 2。参数量小一大圈还能反超,说明"AR 底座加扩散并行"这条路线不是用质量换速度,而是两头都要。

从头训练,还是改造现成 AR 模型

落地路径有两条:可以从头训练一个 Uno,也可以改造已有的开源 AR 模型得到 Uno。前者的想象空间是新架构的原生设计,后者才是真正撬动生态的杠杆——市面上现成的开源 AR 模型都有机会被"点化"成 Uno 形态,不必推倒重练。再叠加 Diffusion Distillation 对现有训练管线的额外开销可以忽略这一点,改造路线的边际成本相当克制。代码与 checkpoint 已经开源,项目页在 s-sahoo.github.io/uno,代码仓库在 GitHub 的 ifm-ai/uno,动手派的材料是齐的。

接入与自测:量一遍首 token 延迟和总吞吐

站在多模型路由这一侧,我不会只看 benchmark 数字就下结论,更习惯把同一条提示词在不同解码路线的模型上各跑一遍,把首 token 延迟和总吞吐量出来再说话。下面这段脚本走 OpenAI 兼容 SDK 风格,base_url 指向中转网关,api_key 从环境变量读取,对两个模型分别做流式请求,记录首 token 延迟、总耗时和总吞吐,方便横向对比不同解码路线的模型:

import os
import time

from openai import OpenAI

# 两个待对比的模型:一条标准 AR 串行解码路线,一条 Uno 扩散并行解码路线
MODELS = {
    "ar_serial": "Qwen/Qwen3-8B",
    "uno_diffusion": "uno/Uno-8B",
}

BASE_URL = "https://4sapi.com/v1"
PROMPT = "用一段话解释分布式锁的常见实现方式,以及各自容易踩的坑。"
MAX_TOKENS = 512


def bench(model: str, rounds: int = 3) -> None:
    client = OpenAI(
        base_url=BASE_URL,
        api_key=os.environ["FOURSAPI_API_KEY"],  # api_key 从环境变量读取,不要硬编码
    )
    ttfts, throughputs = [], []
    for _ in range(rounds):
        start = time.perf_counter()
        first_token_at = None
        total_tokens = 0

        stream = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": PROMPT}],
            max_tokens=MAX_TOKENS,
            stream=True,
            stream_options={"include_usage": True},
        )

        for chunk in stream:
            if getattr(chunk, "usage", None) is not None:
                total_tokens = chunk.usage.completion_tokens
                continue
            if chunk.choices and chunk.choices[0].delta.content:
                if first_token_at is None:
                    first_token_at = time.perf_counter()

        total_time = time.perf_counter() - start
        if first_token_at is None or not total_tokens:
            print(f"{model}: 本轮未取到完整计量,跳过")
            continue
        ttfts.append(first_token_at - start)
        throughputs.append(total_tokens / total_time)

    if not ttfts:
        print(f"{model}: 全部轮次失败,检查模型名与网络配置")
        return
    ttfts.sort()
    throughputs.sort()
    mid = len(ttfts) // 2
    print(f"model={model}")
    print(f"首 token 延迟中位数: {ttfts[mid] * 1000:.0f} ms")
    print(f"总吞吐中位数: {throughputs[mid]:.1f} tokens/s")


for name in MODELS:
    bench(MODELS[name])

跑的时候有几个细节值得留意:必须开流式,不然测到的首 token 延迟是假的;两个模型的提示词和 max_tokens 保持一致,吞吐才有可比性;同一个模型多跑几轮取中位数,能滤掉网络抖动;想在 batch 层面复现吞吐优势,还得再写个并发脚本,把并发数从 1 一路拉上去看曲线。中转站背后若同时挂着标准 AR 模型和 Uno 形态的模型,这段脚本就是最直接的照妖镜。

三条加速路线对照表

把三条路线收进一张表,方便直接抄进选型文档:

解码路线 是否要 draft 模型 是否无损 部署复杂度 适用场景
AR 串行 不需要 无损(基线本身) 最低,单模型走天下 通用兜底;显存极度紧张、流量很小的边缘场景
投机解码 需要,独立 draft model 无损(验证以目标模型分布为准) 高,多一份权重与 KV cache 管理,配对关系随模型数增长 draft 与目标模型适配成熟、追求稳定加速的常驻服务
Uno 扩散并行 不需要 宣称无损(固定上下文长度下,由 Spec 采样器实现) 中,模型本体换成 Uno 形态即可,服务结构不变 延迟敏感的在线服务与吞吐敏感的批处理两头通吃,batch 拉满也赚

表里的部署复杂度是按路由平台视角打的分:AR 串行一个模型走天下最省心;投机解码要为目标模型配 draft,模型清单一长,配对关系就成了运维负担;Uno 只需把模型本体换成 Uno 形态,路由层几乎无感——这也是评估接入顺序时最看重的一类特性:改造发生在模型内部,而不是平台外围。

避坑清单:验证"无损"宣称的 checklist

"无损"两个字,谁说了都不算,得亲手验。我这边的 checklist:

这套流程走完,无损是真是假、加速落在哪个区间,心里基本有数。开源了代码和 checkpoint 的项目,验证门槛本来就低,没理由省这一步。

成本与风险提示

先把收益算明白。延迟计费、按体验说话的产品里,3 倍加速几乎是直接的红利:首 token 延迟降下来,会话节奏变快,工具调用链条的每一步都跟着提速,用户感知最明显。吞吐计费、拼机器利用率的场景里,同样的卡时能消化更多请求,单 token 的基础设施成本被摊薄,账面上是实打实的省。两种账法,这笔加速都有的赚。

风险也得摆上台面。第一,Uno 是新面孔,8B 对 26B 的胜绩不能外推到所有规模和所有任务,接入前按上一节的 checklist 过一遍是底线动作。第二,无损宣称绑定固定上下文长度这个前提,业务里上下文动辄顶到上限的场景要单独压测,别把前提弄丢。第三,改造现成开源 AR 模型的路线虽然诱人,模型清单与 checkpoint 的覆盖面要以开源仓库的实际进度为准,别提前透支想象。第四,推理时扩展和采样器版本都可能改变行为,生产环境务必锁版本、留灰度、设回滚。最后照例提醒:所有接入都走正规 API 与授权渠道,负载均衡与计费优化建立在合规之上,绕过官方限制的方案不碰。

写在最后:把解码路线当成路由参数

收个尾。这一期把 Uno 从头到尾过了一遍:它是一类扩散增强 LLM,参数拆成 AR 权重加轻量扩散权重两组,前者照常做 next-token prediction,后者靠 Diffusion Distillation 从 AR 分布里学并行抽 token,额外训练开销可以忽略;配套的 Spec 采样器家族在固定上下文长度下做无损加速与推理时扩展;不用独立 draft model,也不牺牲 AR 底座的质量;所有评测 batch size 下吞吐都压过主流投机解码,相对基座最高 3 倍加速,8B 的体量在工具调用、编码、长上下文三类基准上全面越过 26B 的 DiffusionGemma 和商用 Mercury 2;可以从头训练,也能改造现成开源 AR 模型,代码与 checkpoint 已经开在 s-sahoo.github.io/uno 与 GitHub 的 ifm-ai/uno。

回到老本行:在 4sapi.com 做多模型路由,"解码路线"这个字段,往后大概要和"价格""速度"并列成模型标签的一栏——同一能力档次的模型,免 draft 的并行解码版本理应排进优先级更高的池子。免 draft、不降质、最高 3 倍加速,是真香还是看走眼?欢迎在评论区说说看法。