Day49|投机解码:小模型帮大模型提速,为什么不一定更快?

苦猿的大模型日记 · Day49 · 投机解码-帮普通人把AI学进简历系列

前言:先猜再验,为什么可能更快

前几天有个读者留言问我:

既然已经让一个小模型先写答案了,为什么不直接用小模型?还要再让大模型检查一遍,这不是多算了一次吗?

这个问题很合理。第一次听到 Speculative Decoding,投机解码 时,我也觉得它像是把一道题先交给小模型做一遍,再交给大模型批改一遍。要是大模型最后还是得自己算,前面那一遍岂不是白忙?

但大模型推理里有一个特别贵的动作:每生成一个 token,都要再跑一次目标模型的 Decode。

一段 300 个 token 的回答,普通自回归解码通常要让目标模型完成很多轮"读入历史、预测下一个 token"的工作。目标模型每轮只往前走一步,GPU 的大矩阵计算却要反复启动。

投机解码换了一个思路:让更快的 Draft Model 先连续猜 4 个、5 个甚至更多 token,再让真正负责质量的 Target Model 一次性检查这一小段。如果这几个猜测大部分都对,目标模型一轮就能向前推进好几步。

反过来,如果 Draft Model 猜得不准,或者它本身就慢得不够"便宜",投机解码就会变成额外工作。

所以它赌的不是"小模型能不能答对",而是:

小模型能不能连续猜中几步,而且猜测成本足够低。

这篇不把投机解码写成一个打开就加速的按钮,而是把它拆成五笔工程账:它到底做了什么,接受率为什么重要,如何写一个不依赖 GPU 的模拟器,真实推理引擎里还要付出什么成本,以及上线前应该看哪些指标。

PART 01:投机解码到底做了什么

1.1 普通解码为什么一轮只走一步

把已经生成的 token 序列记作:

复制代码
y1, y2, y3, ..., yt

普通自回归解码会根据这段历史计算下一个 token。生成后,把它拼回上下文,再计算下一个 token。于是目标模型的调用路径大致是:

复制代码
Target -> y1
Target -> y2
Target -> y3
Target -> y4
...

KV Cache 会避免每一步重复计算已经读过的历史,但它没有改变"每一轮只生成一个新 token"这个事实。输出越长,Decode 轮数越多;目标模型越大,每一轮的成本越高。

1.2 Draft Model 负责猜,Target Model 负责拍板

投机解码引入两个模型:

  • Draft Model:参数量更小、推理更快,用来提出候选 token。
  • Target Model:真正对外提供结果的大模型,负责验证候选,最终采样逻辑仍然由它决定。

设当前上下文是 x,Draft Model 连续提出 gamma 个候选:

复制代码
d1, d2, d3, ..., dγ

然后 Target Model 沿着这条候选路径,计算每个位置的目标分布:

复制代码
p1 = Target(x)
p2 = Target(x, d1)
p3 = Target(x, d1, d2)
...
pγ = Target(x, d1, ..., dγ-1)

简化后,候选 token di 的接受概率可以写成:

复制代码
αi = min(1, pi(di) / qi(di))

目标模型更认可它时,接受概率为 1;目标模型认为它不那么可能时,接受概率按比例下降。如果某个位置拒绝,后面的候选也不能继续沿用,系统会从目标模型的修正分布里采一个新 token,结束这一轮。

1.3 一轮最多能推进多少 token

假设 Draft Model 提议 4 个 token:

复制代码
候选:d1  d2  d3  d4
结果:√   √   √   ×

这一轮会接受 d1、d2、d3,并在第 4 个位置用 Target Model 的分布产生一个 token。目标模型虽然只完成了一次验证前向,但序列向前推进了 4 个 token。如果四个都接受,Target Model 通常还可以在候选末尾贡献一个 bonus token。

情况 候选长度 接受结果 本轮推进 说明
全接受 4 √√√√ 5 4 个草稿 token,加 1 个目标 token
部分接受 4 √√× 3 后续候选作废,目标模型补 1 个
首个拒绝 4 × 1 目标模型直接采 1 个,草稿成本没有转化成收益

这就是投机解码与"先用小模型写答案,再让大模型润色"的差别:前者只在 token 级别做短距离猜测,最终序列仍由 Target Model 的分布控制。

1.4 为什么它不等于质量变差

严格的 speculative sampling 会修正接受/拒绝步骤,使最终采样结果仍然服从 Target Model 的目标分布。它不是把 Draft Model 的答案强行塞给用户。

但工程上仍可能出问题:框架的采样兼容性有限,EOS 和工具调用边界处理不正确,tokenizer 或量化方式不匹配,都会让接受率或输出质量下降。业务如果为了提速偷偷改了采样参数,也已经不是同一个生成分布。

所以质量验收不能只看文本大概一样,还要检查 JSON 合法率、工具调用参数、停止位置和长上下文的关键事实。

PART 02:接受率才是这笔账的核心变量

2.1 不要只问加速几倍,先问每轮推进几步

投机解码最重要的指标不是 Draft Model 的参数量,而是 accepted tokens per step,即每次 Target Model 验证平均能留下多少 token。

常见指标有三个:

  • Acceptance rate:被接受的草稿 token 数 / 草稿 token 总数。
  • Accepted tokens per step:每轮平均接受多少 token。
  • Bonus token rate:候选全部接受后,Target Model 在末尾额外产生 token 的比例。

如果 gamma=4,平均接受率是 0.75,不能机械地说每轮一定接受 3 个。拒绝是从左到右发生的,后面的 token 可能因为前面已经改变而失效。更稳妥的做法是直接统计每轮的 accepted length。

2.2 一个实用的收益模型

记 Cd 为草稿模型单 token 成本,Cv 为目标模型验证 gamma 个位置的成本,Cs 为调度、采样、通信和缓存开销,L 为普通 Decode 单轮成本,K 为投机一轮平均推进 token 数。

复制代码
Tvanilla ≈ N × L
Tspec ≈ (N / K) × (gamma × Cd + Cv + Cs)

只有当:

复制代码
K / (gamma × Cd + Cv + Cs) > 1 / L

投机解码才值得。接受率高,不等于一定加速;Draft Model 足够快,也不等于一定加速。

假设普通解码每轮成本为 10,gamma=4,草稿每 token 成本 0.8,目标验证成本 11,调度成本 1:

复制代码
投机一轮成本 = 4 × 0.8 + 11 + 1 = 15.2

如果一轮平均推进 2 个 token,单位 token 成本是 7.6,理论上有收益;如果 Draft Model 更慢,或者验证成本接近 30,这笔账就会翻负。真实结果还会受 batch size、GPU kernel、显存带宽、跨卡通信和请求长度影响。

2.3 gamma 为什么不是越大越好

gamma 是每轮让 Draft Model 猜多少个 token。猜得越长,一次验证成功后能走得越远;但候选越靠后,分布越容易和 Target Model 拉开差距,草稿成本和临时 KV Cache 也会增加。

  • gamma 太小:接受率高,但每轮节省不了多少 Target Decode。
  • gamma 太大:偶尔全中时很漂亮,但拒绝点可能很靠前,后面的草稿成本全部浪费。

至少比较 gamma=2、4、8,并按任务分桶:

请求类型 常见特征 重点观察
代码补全 局部模式强、重复结构多 连续接受长度、换行和缩进
客服问答 模板化程度较高 短输出是否被草稿开销拖慢
开放式创作 温度高、分布发散 接受率与 P95 TPOT
JSON 抽取 结构约束强 JSON 合法率、拒绝点位置

2.4 接受率会随输出位置变化

全局 acceptance rate 可能掩盖:开头固定模板容易猜,中间事实内容和长尾表达经常拒绝;短回答的前几个 token 占比高,也会放大平均值。

更有用的观测方式是:

复制代码
按输出长度:0-32 / 33-128 / 129+
按任务:代码 / 对话 / JSON / 工具调用
按采样:temperature、top-p
按并发:低并发 / 目标并发 / 高并发

低并发接受率 80%、高并发掉到 45% 时,不要急着换模型,先检查两个模型是否争抢显存,以及批处理是否把候选拆碎。

PART 03:写一个可运行的简化实验

真实投机解码需要 GPU、两个模型和推理框架才能测端到端延迟,但我们可以先用离散 token 分布验证机制。这个模拟器只回答三个问题:接受率变化时每轮能推进多少 token,草稿成本变高后什么时候收益消失,以及同样的 gamma 为什么在不同模型相似度下结果不同。

3.1 最小接受/拒绝实现

下面的代码使用纯 Python 标准库。target_probs 和 draft_probs 是同一组 token 的离散概率分布,speculative_step() 模拟一轮投机解码。

复制代码
from __future__ import annotations

import random
from dataclasses import dataclass
from typing import Dict, List, Sequence

Token = str
Distribution = Dict[Token, float]


def sample_from(probs: Distribution) -> Token:
    tokens = list(probs)
    weights = [probs[token] for token in tokens]
    return random.choices(tokens, weights=weights, k=1)[0]


def normalize(probs: Distribution) -> Distribution:
    total = sum(max(value, 0.0) for value in probs.values())
    if total == 0:
        raise ValueError("probability mass is empty")
    return {token: max(value, 0.0) / total for token, value in probs.items()}


def corrected_distribution(target: Distribution, draft: Distribution) -> Distribution:
    residual = {
        token: max(target.get(token, 0.0) - draft.get(token, 0.0), 0.0)
        for token in target
    }
    return normalize(residual) if sum(residual.values()) else normalize(target)


@dataclass
class StepResult:
    accepted: List[Token]
    generated: Token
    rejected_at: int | None


def speculative_step(
    target: Sequence[Distribution],
    draft: Sequence[Distribution],
) -> StepResult:
    if len(target) != len(draft):
        raise ValueError("target and draft must have the same gamma")

    candidates = [sample_from(probs) for probs in draft]
    accepted: List[Token] = []

    for index, candidate in enumerate(candidates):
        target_probs = target[index]
        draft_probs = draft[index]
        p = target_probs.get(candidate, 0.0)
        q = draft_probs.get(candidate, 0.0)
        acceptance = 1.0 if q == 0 and p > 0 else min(1.0, p / q) if q else 0.0

        if random.random() <= acceptance:
            accepted.append(candidate)
            continue

        replacement = sample_from(corrected_distribution(target_probs, draft_probs))
        return StepResult(accepted, replacement, index)

    bonus = sample_from(target[-1])
    return StepResult(accepted, bonus, None)

这个实现保留了关键动作:Draft Model 先采样候选,Target Model 逐个计算接受概率,拒绝后从修正分布采一个新 token,全接受时再产生 bonus token。

它没有实现真实框架里的 KV Cache 复用、批量 logits、CUDA kernel 和流式输出,所以不要把运行时间当成模型 benchmark。它的价值是让"接受率"从一句口号变成可观察的变量。

3.2 跑四组可控实验

可以让草稿分布由目标分布和均匀分布混合,模拟不同模型相似度:

复制代码
def mix_with_uniform(target: Distribution, similarity: float) -> Distribution:
    tokens = list(target)
    uniform = 1.0 / len(tokens)
    return normalize({
        token: similarity * target[token] + (1 - similarity) * uniform
        for token in tokens
    })


def run_experiment(similarity: float, gamma: int, rounds: int = 2000) -> None:
    target_one_step = normalize({
        "A": 0.45, "B": 0.25, "C": 0.12,
        "D": 0.08, "E": 0.06, "F": 0.04,
    })
    target = [target_one_step for _ in range(gamma)]
    draft = [mix_with_uniform(target_one_step, similarity) for _ in range(gamma)]

    accepted = 0
    progressed = 0
    rejected_rounds = 0

    for _ in range(rounds):
        result = speculative_step(target, draft)
        accepted += len(result.accepted)
        progressed += len(result.accepted) + 1
        rejected_rounds += result.rejected_at is not None

    print({
        "similarity": similarity,
        "gamma": gamma,
        "acceptance_rate": round(accepted / (rounds * gamma), 3),
        "avg_progress": round(progressed / rounds, 3),
        "reject_round_rate": round(rejected_rounds / rounds, 3),
    })


if __name__ == "__main__":
    random.seed(7)
    for similarity in (0.35, 0.65, 0.90):
        for gamma in (2, 4, 8):
            run_experiment(similarity, gamma)

相似度提高时,接受率和平均推进 token 数通常上升;gamma 增大后,平均推进可能先增加,再因为拒绝变多而趋于平缓。

这段代码没有给出"固定加速 1.8 倍"的结论。要估算 speedup,还要把成本加入模型:

复制代码
def estimated_cost(
    rounds: int,
    gamma: int,
    draft_cost: float,
    verify_cost: float,
    schedule_cost: float,
) -> float:
    return rounds * (gamma * draft_cost + verify_cost + schedule_cost)

只要把 draft_cost 提高,或者把 verify_cost 设置得接近多个普通 Decode 轮次,原本漂亮的加速曲线就会掉下来。

3.3 这个模拟器没有告诉你的事

它没有告诉你真实 GPU 上的 kernel 是否高效,也没有模拟:

  • 两个模型是否能在同一批次中高效调度。
  • 草稿被拒绝后,临时 KV Cache 如何回收。
  • 双模型占用显存后,其他请求是否被迫排队。
  • 跨卡部署时,候选 token 和隐藏状态传输需要多少时间。
  • 流式输出时,用户是否会感知到验证批次的节奏变化。

正确顺序是:先用模拟器理解变量,再用离线请求集测接受率,最后在目标硬件上做端到端压测。

PART 04:真实推理引擎里,速度不是只由算法决定

4.1 Draft Model 放在哪里

最简单的部署方式是同一进程、同一张 GPU 上运行两个模型。好处是通信少;坏处是显存压力大,两个模型会争抢计算和 KV Cache。

第二种方式是把两个模型放到不同 GPU。目标模型显存更充足,但每轮投机都要跨卡传递候选或中间结果。并发提高后,通信排队和同步会进入 P99。

第三种方式是把 Draft Model 做成独立服务。它便于弹性扩缩容,也更容易给多个 Target Model 共享,但网络、序列化、请求关联和失败重试都要计入成本。

部署方式 优点 主要风险
同卡同进程 通信少、实现简单 显存和算力争抢
双卡部署 目标模型空间更充足 跨卡同步、资源利用率不稳定
独立 Draft 服务 弹性和复用性好 网络延迟、服务级联、故障处理

4.2 KV Cache 预算不能只看模型权重

投机解码会产生一段候选路径。Target Model 验证时,系统需要为候选位置准备临时状态,并在接受或拒绝后保留正确前缀、释放无效分支。

显存预算至少要分成:

复制代码
模型权重
+ active request 的 KV Cache
+ Prefix Cache 或其他共享缓存
+ speculative decoding 的临时状态
+ 框架运行时和通信缓冲

如果只看到 Target Model 的 KV Cache 还能放下 32 个请求,就直接开启投机解码,很可能在高并发时遇到抢占、驱逐或 OOM。

4.3 它和 Continuous Batching 如何相处

投机解码让某些请求一次推进多个 token,会改变批内请求的节奏:

  • 短请求可能更快完成,较早释放 KV Cache。
  • 长请求可能连续占用验证预算,影响其他请求的公平性。
  • 不同请求的 gamma 不同,批处理形状更不规则。
  • 验证批次变大,吞吐可能改善,但单请求等待时间也可能上升。

因此要同时看 TPOT、P95/P99 延迟、完成请求数和 GPU 利用率。单请求 benchmark 上的加速,不代表混合长短请求后的服务体验也会改善。

4.4 它和 Prefix Cache 解决的是不同阶段

Prefix Cache 主要减少不同请求之间重复的 Prefill;投机解码主要减少单个请求 Decode 的轮数。一个请求可以先从稳定前缀命中点开始,再用 Draft Model 猜后面的输出。

但两者也会争抢显存。Prefix Cache 留得太多,可能挤压投机临时状态;候选开得太长,也可能让前缀缓存的可用容量下降。

比较稳妥的调优顺序是:

  1. 先固定 Prompt 结构,测清楚 Prefix Cache 的复用收益。
  2. 关闭投机解码,建立 vanilla decoding 的真实 baseline。
  3. 在相同缓存和并发配置下,逐步增加 Draft Model 与 gamma。
  4. 最后评估两种优化同时开启时,P99 和 goodput 是否仍然改善。

4.5 采样参数会改变接受率

温度越高,Target Model 的分布通常越发散;top-p 截断也会改变候选 token 的概率质量。Draft Model 如果没有使用一致的 tokenizer、采样策略和停止条件,分布差异会直接反映到接受率上。

建议把这些参数写入结果文件:

复制代码
model / draft_model / tokenizer
temperature / top_p / top_k
gamma / max_new_tokens
hardware / dtype / quantization
concurrency / prompt_tokens / output_tokens

否则下一次换了采样参数,发现速度变化,很难判断是 Draft Model 不合适,还是请求分布变了。

4.6 推理框架落地不要照抄一条命令

不同版本、不同后端对 speculative decoding 的支持方式并不完全一致。有的需要指定 Draft Model,有的通过 speculative config 传递候选长度,有的还区分 n-gram、独立草稿模型和特定架构。

概念性的配置骨架可以写成:

复制代码
{
  "draft_model": "<small-model>",
  "num_speculative_tokens": 4,
  "method": "draft-model",
  "temperature": 0.2
}

启动前要用目标版本的 --help 和官方文档确认字段名称、后端限制和采样兼容性。真正的验收不是服务启动成功,而是日志里能看到候选 token、接受 token、回退次数和目标模型 forward 次数发生变化。

PART 05:哪些任务适合,哪些任务不适合

5.1 更容易受益的任务

代码补全通常是好候选。代码有明显局部模式,缩进、括号、常见 API 调用和重复结构让相邻 token 的分布更集中。

结构化输出也有机会受益。JSON 的括号、字段名和固定格式让候选路径更规整,但质量验收必须严格:少一个逗号、提前闭合一个括号,都可能让业务解析失败。

模板化客服问答在低温度下可能有不错的接受率,尤其是开头的礼貌语和固定流程。不过回答较短时,草稿成本占比会变高。

5.2 不一定受益的任务

开放式创作、强随机采样和频繁调用工具的 Agent 轨迹,通常更难保持连续猜测。工具调用前后的上下文变化、参数校验和外部结果插入,都会让候选迅速过时。

短回答也要谨慎。假设用户只需要 12 个 token,即使投机解码把每轮推进从 1 提升到 3,启动、同步和缓存管理成本可能已经占掉大部分收益。

Draft Model 和 Target Model 参数量接近,也不代表分布接近。训练数据、词表、对齐方式和领域风格的差异都会影响接受率。

5.3 低接受率时的症状

当投机解码不适合当前流量时,通常能看到:

  1. accepted_tokens / draft_tokens 很低,按任务分桶后仍低。
  2. Target Model forward 次数没有明显下降。
  3. GPU 利用率上升,但 TPOT 和 P99 变差。
  4. Draft Model 占用了显存,却没有换来更高的 goodput。
  5. 拒绝点集中出现在工具调用、JSON 边界或长上下文事实回答处。

这时不要只把 gamma 调大。先确认请求类型是否适合,再比较 Draft Model,最后才调 gamma 和并发。

5.4 一个上线前的场景矩阵

场景 预计接受率 建议 gamma 初步判断
低温代码补全 4-8 值得优先试验
固定模板客服 中高 2-4 关注短输出收益
JSON 抽取 2-4 质量门槛高,需严格回归
长文开放式创作 低到中 2 先小流量验证
多轮工具调用 波动大 1-2 默认关闭,按路由灰度

这张表只能帮助你排实验优先级,不能替代真实流量数据。最终决定权应该交给 acceptance rate、TPOT、P99、质量回归和 goodput。

投机解码不是速度开关,而是一笔随输出分布波动的风险投资。

PART 06:上线前的验收清单

6.1 先建立不带投机的 baseline

至少固定这些条件:

  • Target Model、tokenizer、dtype 和量化配置。
  • Prompt token 数、输出长度分布和停止条件。
  • temperature、top-p、top-k 等采样参数。
  • GPU 型号、并发数、请求到达率和 Continuous Batching 配置。
  • Prefix Cache 是否开启,以及缓存容量和命名空间。

没有 baseline,看到 tokens/s 上升时,不能确定是投机解码带来的,还是并发、缓存命中或请求变短造成的。

6.2 必须记录的指标

指标 用来回答什么问题
draft_tokens 草稿模型实际猜了多少 token
accepted_tokens Target Model 接受了多少
acceptance_rate 猜测和目标分布是否接近
accepted_tokens_per_step 每轮真正向前推进多少
target_forward_calls 目标模型调用轮数是否下降
fallback_count 多少请求切回普通解码
KV cache watermark 投机状态是否挤压其他请求
P50/P95/P99 TPOT 用户等待是否真的改善
goodput 满足 SLO 的完成请求数是否增加

其中容易漏掉的是 fallback_count。有些系统在接受率很低时自动退回 vanilla decoding,只看总吞吐会误以为投机配置一直在工作。

6.3 灰度和自动回滚

不要一上来把所有流量切到投机解码,可以先按请求类型或租户灰度:

复制代码
离线请求集
  -> 低并发线上回放
  -> 5% 真实流量
  -> 20% 目标任务流量
  -> 按 acceptance rate 和 P99 自动升降级

连续多个时间窗口中,某类请求的 acceptance rate 低于阈值,或者 P95/P99 TPOT 相比 baseline 恶化,就关闭该路由的 speculative decoding;质量回归失败时直接回滚,不等待性能指标。

上线门槛最好写成具体条件:

  1. 目标任务的 P50/P95 TPOT 有稳定改善。
  2. P99 没有超过现有 SLO,高并发下不出现大幅抖动。
  3. JSON 合法率、工具调用成功率和关键事实准确性通过回归。
  4. KV Cache 保留足够余量,OOM、抢占和驱逐没有增加。
  5. goodput 提升,而不是只有 GPU 利用率和峰值 tokens/s 变好。

6.4 十项上线前检查

  • Target Model 与 Draft Model 的 tokenizer、EOS 和模板配置一致。
  • 已保存 vanilla decoding 的可复现 baseline。
  • 已按任务类型统计 acceptance rate。
  • 已测试 gamma=2、4、8 或对应候选范围。
  • 已测短输出、长输出和高并发混合流量。
  • 已记录两个模型和临时 KV Cache 的显存水位。
  • 已验证温度、top-p、top-k 等采样参数兼容性。
  • 已验证 JSON、工具调用和停止条件。
  • 已配置低接受率和 P99 恶化时的自动回滚。
  • 已把成功、拒绝、fallback 和异常样本留存下来复盘。

结尾:猜得准才有资格提速

投机解码真正减少的,不是模型要生成的 token 数,而是 Target Model 为了生成这些 token 必须执行的 Decode 轮数。

它把一次大模型推理拆成两个角色:一个负责快速提出短期猜测,一个负责用更高质量的分布做验证。猜得准、猜得快、缓存和调度扛得住,才有可能把"每轮一个 token"变成"每轮多个 token"。

如果接受率低,或者草稿模型本身不够便宜,那些看起来很聪明的候选 token,最后只会变成另一笔延迟账单。

真正值得记住的三条原则是:

  1. 先用真实流量测接受率,不要凭参数量猜收益。
  2. 把 gamma、采样参数、KV Cache 和 batching 放进同一张成本表。
  3. 用 P95/P99、质量和 goodput 验收,而不是只展示 tokens/s 峰值。

推理优化的关键,不是让模型永远少算,而是让每一次少算都能被指标证明。

互动时间:如果给你的业务配一个 Draft Model,你最想先试代码补全、客服问答,还是结构化抽取?

下一篇预告:当 Prefill 和 Decode 开始互相争抢 GPU,接下来要拆的是 Prefill/Decode 分离与 disaggregated serving。

--- END ---

苦猿 · 帮普通人把 AI 学进简历

相关推荐
迪康coolmu1 小时前
企业文件外发防泄漏实践:从通道封堵到三层全链路管控
大数据·运维·网络·人工智能·安全·阿里云
听你说321 小时前
三诺动态血糖仪i6搭载L5级小诺智能体,开启AI控糖新时代
人工智能
TMT星球1 小时前
AI智融·场景共生,奥维云网2026数字AI生态大会在南京圆满举办
人工智能
艾莉丝努力练剑4 小时前
【AI接入大模型SDK】ChatSDK示例验证
人工智能·学习·面试·大模型·sdk
wulitoud4 小时前
把 Claude、Codex、Gemini 的引擎换成本地模型:免费、离线、数据不出本机
人工智能·llama·claude·本地模型
火山引擎开发者社区7 小时前
TLS for DeepSeek Harness 可观测实践:从系统总览到会话复盘
人工智能
数字融合9 小时前
透明化地铁线视频孪生综合监控项目技术
大数据·人工智能·virtualenv
鼎艺创新科技10 小时前
不依赖 UE/Unity:我们如何从零搭建一套国产三维 GIS 渲染引擎
人工智能·算法·unity·游戏引擎·三维电子沙盘
十三画者10 小时前
【文献分享】ConfRetro:融合3D构象信息的逆合成预测Transformer框架
人工智能·深度学习·数据挖掘·数据分析·transformer·数据可视化