时间点过程(Temporal Point Process,TPP)在事件序列建模里一直是很经典的一类工具。从最早的自激励 Hawkes 过程,到后来 Transformer 架构被引入这个领域,模型的表达力确实上了一个台阶,但到了推理阶段的采样效率,却始终是个让人头疼的瓶颈。最近我看到一个很有意思的方向,把自然语言生成里的推测解码(Speculative Decoding)思路搬到了 Transformer 点过程采样上,项目题录叫 TPP-SD,目标很明确:让 Transformer 点过程在生成未来事件序列时不再一步一卡,而是能像大模型加速推理那样,用一个小模型探路、大模型批量验证,整体把采样速度提上去。
这篇博客我想从头把这个事拆一遍:要解决的问题背景、推测解码的原理、TPP-SD 的核心设计思路、以及落地时容易踩的坑。适合正在做事件序列建模、点过程推理加速,或者对推测解码跨领域应用感兴趣的读者。不管你是刚入门还是已经跑过 TPP 模型,读完你应该能对“为什么采样慢、怎么用推测解码加速、实际工程里要注意什么”有一个完整且可落地的认识。
1. 这个项目解决的是什么问题
1.1 时间点过程在建模什么
时间点过程是一类用来建模“连续时间上离散事件”的随机过程。你有一串事件,每个事件带一个时间戳和一个类型,比如用户在一段视频上的弹幕发布记录、一台服务器上连续发生的故障告警、一个 APP 里的用户点击行为流。我们的目标是从历史事件序列里学出未来的演化规律:给定已经发生的 (t_1, t_2, \dots, t_k),预测下一个事件在什么时候发生、是什么类型。
经典的 Hawkes 过程用一条预设的核函数来描述历史事件对未来的自激发影响,形式漂亮、可解释性强,但表达力有限。后来大家开始用神经网络替代这个核函数:RNN、注意力机制都试过,再后来 Transformer 直接被拿来编码整个事件历史。每个历史事件经过 self-attention 得到上下文表示,再通过一个头部网络输出下一个事件的分布。这类模型通常叫做 Transformer-based TPP,常见的有自注意力 Hawkes 过程、注意力点过程这类结构。
这些模型在拟合能力和长程依赖上确实强,但它们在生成阶段有一个共同痛点:采样速度跟不上。
1.2 采样为什么成了瓶颈
TPP 模型训练的时候,我们一般用极大似然或者变分目标,优化的对象是每个事件的条件强度函数或者下一个事件的预测分布。训练完之后,到了模拟、预测、反事实分析这些场景,得从模型里不断采样出新事件。这个采样过程是自回归的:生成了第 (k+1) 个事件,才能去预测第 (k+2) 个。每一步都要做一次完整的 Transformer 前向传播,耗时随序列长度线性累积。
问题还不只是 Transformer 前向传播本身贵,TPP 的采样分布往往是连续的。很多 TPP 模型给出的下一个事件时间是一个条件分布,可能是带复杂归一化项的强度函数转换而来,也可能是一个隐式定义的分布。想从这样的分布里采一个时间点,不能像分类任务那样直接 argmax 或者从类别分布里采样,得做逆变换采样,或者做数值积分、二分查找,甚至用 MCMC 类的迭代方法。这一套组合下来,生成一条长度为几百的事件序列,可能要跑几千次强度函数求值和若干次反向的数值求解,整体的计算耗时完全不可控。
我实际跑过类似的项目,印象很深的一点是:训练一个 Transformer TPP 一百个 epoch 也就一个晚上,但用它做一次蒙特卡洛模拟,生成 100 条长度为 300 的序列,反而经常要等好几个小时。模型的参数越多、历史事件越长,这个耗时越会爆炸。
1.3 加速思路的整体框架
想解决采样慢的问题,可以从几个层面入手。一是改进目标分布本身,比如让模型输出一个解析可采样的分布,但这会牺牲表达力。二是用更高效的数值采样方法,比如自适应拒绝采样、逆 CDF 插值,但这属于微观优化,治标不治本。三是在模型结构上做文章,减少前向传播的次数,而这正是推测解码擅长的方向。
推测解码最早出现在自回归语言模型的推理加速里,核心思路是:用一个更小更快的草稿模型先自回归生成一串候选 token,再让大模型一次性并行验证这些 token,如果验证通过就接受一大段生成结果;如果不通过,就在出错的位点修正。这样一来,大模型只需要做少数几次并行前向传播,而不是逐 token 地串行生成。
TPP-SD 的创新点,就是把这套“以小博大”的思路扩展到 Transformer 点过程的采样场景。难点在于:语言模型处理的是离散 token,点过程要处理的是连续事件时间和离散事件类型的混合结构;语言模型只需要验证下一个词的概率分布,点过程需要验证整个条件强度函数或事件分布。这些区别决定了 TPP-SD 不能简单地照搬 NLP 里的实现,必须在草稿生成、验证准则、采样分布几个环节分别做适配。
2. 推测解码的原始机制
2.1 来自大语言模型的启发
推测解码的核心假设其实很朴素:Transformer 生成的很多 token 都是“确定性较高”的,比如一个常见的搭配出现时,下一个 token 翻来覆去就是那几个候选。既然如此,我何必让大模型一个 token 一个 token 地算?我可以先找个便宜的模型来猜,猜完一长串,再让大模型一次性“批改作业”,对的留下、错的改掉,这样大模型一次前向就能推进多个 token。
这套机制在 NLP 里已经被验证非常有效。如果你的草稿模型和目标模型分布足够接近,实际加速比可以达到 2-3 倍,甚至更高。关键收获是:加速不仅来自草稿模型本身快,更来自目标模型的前向次数从“序列长度 O(T)”降到“验证次数 O(T/K)”,而验证又是并行批处理,等于一次调用验证了 K 个位置。
2.2 草稿模型-目标模型的验证流程
标准的推测解码流程可以拆成四步:
- 草稿模型在当前上下文中自回归采样 K 个候选 token。
- 目标模型把这 K 个候选 token 的输入一次性拼成一个批量,做一次前向传播,得到每个位置的预测分布。
- 从第一个位置开始,用拒绝采样规则逐步对照草稿分布和目标分布,接受那些概率足够一致的 token;碰到第一个不一致的位置就停下来。
- 接受所有通过校验的前缀,然后在第一个不一致的位置,从目标分布里重新采样一个 token,并把这个 token 作为新的上下文起点。
这里用到的接受规则是:对于每个位置,草稿分布给出概率 (q_i),目标分布给出概率 (p_i)。以概率 (\min(1, p_i / q_i)) 接受草稿 token;如果被拒绝,则从修正后的分布 (\text{max}(0, p_i - q_i)) 中采样一个替代 token。这个规则保证了整个生成过程的最终分布仍然严格等于目标模型的分布,理论上没有引入采样偏差。
2.3 为什么这样能加速
从计算量上看,假设候选长度 K=4,草稿模型生成 4 个 token 需要 4 次小型前向,目标模型验证只需要 1 次大型前向。如果换成纯目标模型自回归,生成 4 个 token 需要 4 次大型前向。哪怕草稿模型在生成过程中被中途拒绝了几次,只要平均接受数超过 1,整体计算量就有显著下降。
实际工程里,草稿模型可以是目标任务模型的小蒸馏版本,也可以是同一个模型的浅层。草稿模型越快、和目标模型分布越接近,收益越大。但要注意:草稿模型如果过小,猜得不准,拒绝率会很高;如果过大,它自己的推理成本就把收益吃掉了。通常候选长度 K 在 4 到 8 之间比较合适。
3. TPP-SD 的方案设计拆解
3.1 连续时间分布带来的第一个麻烦
TPP 和语言模型最大的区别在于输出空间。语言模型输出的词表是离散的、有限的,可以直接算概率分布;TPP 输出的下一次事件时间是一个连续值,可能是任意正实数,对应的是一个概率密度函数,而不是离散概率质量函数。
推测解码里的拒绝采样准则依赖离散概率比值 (p_i / q_i)。如果直接把密度函数 (f_p(x)) 和 (f_q(x)) 拿来算 (\min(1, f_p(x)/f_q(x))),在数学上也能成立,但实际效果会很差:连续密度函数的比值可以非常大也可以非常小,接受率往往不稳定;而且目标模型未必能直接给出一个可解析求值的密度函数。
我见过一些项目尝试用强度函数做连续形式的接受拒绝采样,但强度函数在事件密集区间会变得很大,在稀疏区间接近零,数值上很容易溢出。所以 TPP-SD 这一类方案更常见的做法是:先把连续时间轴转换成离散候选,再复用语言模型里的整套推测解码框架。
3.2 时间轴离散化:选桶还是选分位点
把连续时间变离散,最常见的方法是分桶。把事件间的时间间隔划分为若干个区间,每个区间是一个类别。这个映射要么用等宽分桶,要么用训练数据里的分位数分桶。
等宽分桶实现简单,但对数据分布不均匀的时候很不友好——事件密集的区间一个桶里可能塞了几万个样本,稀疏区间可能一个样本都落不进去。分位数分桶则保证每个桶里落入的训练样本数量大致相等,采样时每个桶的初始化概率更均匀,对后续的推测解码更友好。
桶数 N 通常设置在 50 到 200 之间。选得太少,时间精度损失明显,模拟出来的事件时间全在桶边界上挤着,后续分析会失真;选得太多,每个桶的概率都很小,草稿模型和目标模型的概率比对起来更不稳定,接受率下降。这个选择需要结合具体数据的尺度做实验,一般先用分位数分桶试 100 个桶,观察接受率和模拟误差再微调。
3.3 草稿模型怎么选
草稿模型是 TPP-SD 里最灵活的组件。理论上,任何比目标模型更快、序列生成能力差不多的模型都可以。常见的选择有三种:
- 目标 Transformer 的小版本:隐藏层维度减半、层数减少,甚至只保留两三层。好处是训练方式一致,分布天然接近;坏处是它的前向传播还是要吃完整的注意力计算,省不了太多常数级开销。
- 轻量序列模型:比如一个小型 LSTM 或 GRU,作为草稿模型单独训练。这类模型自回归生成速度比 Transformer 快很多,但是捕捉长程依赖的能力弱,如果序列事件间隔很长、依赖很深,草稿质量会下降。
- 被蒸馏的学生模型:直接用目标模型输出的分布作为监督,训练一个小模型去拟合。这是 NLP 里最常用的方式,能尽可能让草稿分布接近目标分布,接受率更容易做上去。
我个人的建议是:如果历史序列长度普遍在几十以内,优先级是小型 LSTM 大于小 Transformer,因为轻量模型的开销优势非常明显;如果序列动辄几百上千,还是得用小 Transformer,否则草稿模型自己就丢失了长程信息,生成的候选跟目标分布差距太大,验证时基本全被拒绝,等于白跑。
3.4 类型和时间的联合采样
一个完整的 TPP 事件是时间加类型。只采样时间、不采样类型,那生成序列没法闭合;只采样类型不考虑时间,也没法用于模拟。所以推测解码在 TPP 上必须处理联合结构。
常见做法是把时间桶和类型拼成一个复合 token 空间。比如桶数是 100,事件类型数是 10,那么复合类别总数就是 1000。草稿模型每步输出一个复合类别,等价于同时决定了“在哪段时间范围内、发生什么类型的事件”。验证时,目标模型对这个 1000 维的分布和草稿模型做概率对照。
这个做法有个可以直接预见的副作用:复合空间里很多组合是极少出现的。比如某些事件类型在深夜几乎不出现,某些时间段本身概率很低,这会导致大量候选的概率接近零。草稿模型如果在这些低概率组合上乱猜,验证时很容易拒绝。缓解的办法是在草稿模型里就引入类型和时间的条件独立性假设,让草稿模型分别采样时间桶和类型,再联合成一个候选。这样草稿分布更平滑,接受率也更稳定。
4. 实操过程与关键参数调优
4.1 环境与基础训练
先说基础环境。Transformer TPP 模型的训练框架可以直接复用常规序列模型训练流程。输入是带时间戳的事件序列,每个事件的特征表示由时间间隔向量和类型向量拼接而成。模型结构方面,事件历史经过位置编码和时间编码后进入多头注意力,输出端接一个强度函数解码头。
训练的时候,我们需要准备的是一组模拟或真实的事件序列。真实数据集典型的有社交互动序列、金融交易序列、医疗事件序列。如果是用来复现和验证推测解码机制,模拟数据反而是更好的选择:因为真实数据的分布未知,你不知道模型学得好不好;模拟数据的生成强度函数是你预设的,你可以对照采样的边际分布是否有偏。
草稿模型可以基于目标模型的训练集同时训练,也可以等目标模型训完后,用它生成的分布作为软标签去教草稿模型。前者实现简单,后者通常接受率更高。我实际比较下来,用软标签蒸馏得到的草稿模型,平均接受率能高出大概 5-10 个百分点,这个差距在长序列生成时非常关键。
4.2 核心参数:候选长度 K
候选长度 K 决定了每一次验证窗口有多大。K 太大,草稿模型一次要生成很长一串,概率连乘之后很难保证后半段的准确性,验证时经常到第二三个位置就被拒绝,白白浪费了草稿模型的生成长度和目标模型的验证计算;K 太小,比如 K=1,那加速比趋近于 1,几乎没有意义。
经验上的起手式是 K=4。然后跑一组接受率统计:记录每个验证窗口内实际接受的平均 token 数。如果平均接受数明显低于 K 的一半,说明草稿质量跟不上,要么缩短 K,要么加强草稿模型的训练;如果平均接受数接近 K,说明草稿模型已经训得太接近目标模型,可以考虑适度提高草稿模型的速度(换更小的结构)或者加大 K 来摊薄开销。
规范化地衡量加速比,可以比较两种采样策略在生成同样数量序列时消耗的目标模型前向次数。假设纯目标模型自回归需要 T 次前向,用 TPP-SD 之后大约是 T / (平均接受数) 次验证前向。这个数通常比直接看运行时间有意义,因为它去掉了硬件和实现的干扰。
4.3 评估指标和效果验证
评估一套采样加速方案,不能只看快不快,还要看采样结果有没有变“坏”。有两类指标需要同时盯紧。
第一类是分布校准度。用模型生成一批模拟序列,再统计事件时间的边际分布、事件类型的频率分布、事件强度的自相关,跟目标模型直接自回归生成的结果做对比。如果两者分布明显不一致,说明你的推测解码实现可能在接受准则上写错了,或者分桶方案引入了偏差。
第二类是下游任务表现。如果这个采样结果是用来做预测或规划的,那最终的误差、准确率都要跑一遍完整流程,不能只验证采样分布相似就收工。比如你拿模拟序列做事件爆发的预警,如果采样加速后预警延迟变大,哪怕边际分布看起来没问题,也可能是联合概率在小概率事件上出了偏差。
我自己在项目里的标准做法是:先跑 200 条固定种子序列做极差对比,确认分布一致后,再上完整的批量模拟。这个流程能帮你区分“实现 bug”和“正常的加速误差”。
5. 常见问题与排查实录
5.1 接受率低到离谱
如果你发现生成一个超长事件序列的耗时几乎没降,甚至比纯自回归还慢,那大概率是平均接受数在 1 以下。排查顺序如下:
- 先看草稿模型训练得够不够:把草稿模型单独拿出来做下一个事件的预测准确率,如果远低于目标模型,那就是草稿太弱。解决办法是换成更强的草稿结构,或者用蒸馏软标签重新训练。
- 再查候选长度:如果 K 设得过大,后半段候选全是噪声。降低到 3-4 重新测接受率。
- 最后查时间分桶:分桶太细会让每个桶的概率都很小,概率比值方差大,导致接受率下降。粗分桶再试一下。
5.2 概率比值出现 NaN 或无穷大
连续概率密度和强度函数的数值范围波动很大,尤其在事件密集区间,强度函数可以膨胀到几百,密度比值直接冲上无穷大。处理办法是全程用 log 概率计算,拒绝接受规则用 log-sum-exp 形式重写,同时在时间分桶方案里尽量避开过细的区间,从源头控制概率值的动态范围。
还有一个小坑:草稿模型和目标模型分布都用 softmax 输出时,如果其中一个模型的 logits 没做标准化就进入接受率计算,数值会极具不稳定。常规做法是保证两边都先过一层完整的 log-softmax,再做比值。
5.3 候选验证的批量计算不够快
推测解码的“验证”在理论上是一次并行前向,但实现时如果在同一个批次里塞入了 K 个长度不等的序列,padding 会造成大量无效计算。如果你的草稿模型生成候选很快,但目标模型验证时因为 padding 浪费严重,整体收益会被抵消。
解法有两个方向。一是用变长批处理框架,按最长有效长度打包,而不是统一 padding 到固定长度;二是套用 decode-only 架构常见的 KV cache 模式,在验证时只计算新增位置,不重复计算历史。这两个方向对 Transformer TPP 的高效实现同样适用,建议优先做的还是 KV cache,因为事件历史经常很长,重复计算历史 attention 的开销比 padding 更致命。
5.4 类型和时间出现错位采样
复合 token 空间里,时间桶和类型是独立映射的。假设某个时间桶的候选通过了验证,但类型被拒绝,你重新采样时可能会落到“同一时间桶、另一类型”的组合上。这会破坏事件类型和时间之间的相关性。比如在服务器故障事件里,高负载时段更容易出现磁盘告警,如果类型和时间被割裂采样,模拟出来的联合分布就不对。
解决方法是把验证规则改成对“时间桶-类型”整体做接受拒绝,不要拆开验证。如果确实想拆开,也需要在拒绝后从联合修正分布中采样,而不是分别独立重采样。这个细节看起来小,但在后续做联合预测时影响很大。
6. 从 NLP 到 TPP:跨领域迁移的通用经验
TPP-SD 这个工作,表面上是一个拿推测解码加速点过程采样的工程优化,但它的设计过程其实具普适性:当你有一个领域模型推理速度太慢,而另一个领域恰好有一套成熟的加速机制时,怎么判断机制能不能迁移、怎么改造它?
我的体会是要抠三个东西。第一是分布的类型:离散分布可以直接套接受拒绝框架,连续分布必须先离散化或者设计连续的统计量,这是绕不开的第一步。第二是验证的成本结构:推测解码的好效果来源于“一次验证多个位置”,你得先确认你的目标模型支持这种批量计算,如果模型结构是串行强依赖的,收益会打折扣。第三是误差的可控性:迁移出来的方案必须确保生成分布无偏,否则下游任务会继承这个偏差,结果可能比慢一点更糟糕。
拿这三个标尺回头来看,TPP 和语言模型确实非常适合做这种迁移:Transformer 结构一样,自回归生成方式一样,离散类型可以类比 token,连续时间可以通过分桶映射到离散空间。这也是 TPP-SD 能成立的根本原因。
以我个人的实践经验,最容易被忽视的反而是最后一点:误差可控性。因为加速带来的收益是立竿见影的,跑几次模拟就能看到时间下降;但分布偏差往往是潜移默化的,不专门做统计检验根本发现不了。我建议在每个加速项目里把“分布一致性校验”做成固定步骤,宁可多花半天做测试,也不要带着潜在偏差上线。毕竟在事件序列建模这块,慢一点换来稳定可靠,永远比快很多但结果可疑要值得。