返回知识盲盒

知识盲盒

第四章 解码循环、Streaming 与终止状态机

ai-generatedstochastic-parrotstochastic-parrot-anatomyai

上一章定义了如何从一列 logits 选出一个 token。本章把这个局部转移嵌入完整请求:模型状态、解码状态、增量反分词、停止串缓冲、调度与取消必须一起前进,并在唯一终态收束。

4.1 请求生命周期

一个文本生成请求至少经历以下状态:

CREATED -> QUEUED -> PREFILLING -> DECODING -> DRAINING -> FINISHED
               |          |            |           |
               +----------+------------+-----------+-> FAILED
                          cancel request
                                |
                                v
                       CANCEL_REQUESTED -> CANCELLED

这只是允许转移图,不表示取消可以抢占任意设备指令。状态含义如下:

状态已成立的事实允许的下一动作
CREATED请求对象已创建验证、拒绝、入队
QUEUED尚未占用模型执行槽调度 prefill、取消、超时
PREFILLING正计算提示 cache完成、失败、登记取消
DECODING至少有下一步 logits 或在计算下一步选择、decode、登记取消
DRAINING不再选择 token,仍有安全字节/终态事件待发送发送完成、连接失败
CANCEL_REQUESTED取消已被接受但资源可能仍在执行到安全点清理
FINISHED正常终止原因已固定只允许读取结果
CANCELLED取消终态已固定只允许读取部分结果/元数据
FAILED错误终态已固定只允许读取错误/部分结果

FINISHED、CANCELLED、FAILED 互斥且不可逆。进入任一终态后,不得再发送普通文本增量。

4.2 步首状态与 off-by-one 约定

设提示为 x1:mx_{1:m},已经选出的输出为 y1:t−1y_{1:t-1}。第 tt 次选择之前定义:

StepState_t = {
    cache,                  # covers x[1:m] + y[1:t-1]
    raw_logits_t,           # next-token logits for the same prefix
    decoder_state_t,
    detokenizer_state_t,
    stop_buffer_t,
    stream_sequence_number,
    lifecycle = DECODING
}

条件分布写为

pθ(yt∣x1:m,y1:t−1).p_\theta(y_t\mid x_{1:m},y_{1:t-1}).

选择 yty_t 后有两条路径:

  1. 若 EOS、停止串、长度、取消或错误使请求终止,不需要再计算 zt+1z_{t+1};
  2. 若继续,decode_one(cache, y_t, position=m+t-1) 把 yty_t 的 K/V 追加到所有层,并返回 zt+1z_{t+1}。

因此“已选 token 数”可以比“cache 中输出 token 数”多 1,但只存在于选择后、下一次 decode 前的短暂状态。事件记录应区分 token.selected(t) 与 decode.completed(t)。

4.3 完整的单路径循环

下面给出参考伪代码。它省略设备并行,却不省略影响返回字节的顺序:

function generate(request):
    transition(CREATED, QUEUED)
    normalized = normalize_request(request)
    inputs = assemble_input(normalized)

    check_cancel_or_deadline()
    transition(QUEUED, PREFILLING)
    logits, cache = prefill(inputs)
    decoder = init_decoder_state(normalized.generation_config)
    text = init_incremental_detokenizer()
    stopper = init_stop_matcher(normalized.stop_sequences)
    stream = init_ordered_stream()
    transition(PREFILLING, DECODING)

    while true:
        if cancellation_requested():
            transition(DECODING, CANCEL_REQUESTED)
            break
        if deadline_exceeded():
            finish_reason = "deadline_exceeded"
            break

        token, decision_record = choose(logits, decoder)
        record token.selected(token, decision_record)
        decoder.consume(token)

        if token_is_eos(token):
            finish_reason = "eos"
            break

        new_bytes = text.push(token)
        safe_bytes, stop_hit = stopper.push(new_bytes)
        stream.emit_delta_if_nonempty(safe_bytes)

        if stop_hit:
            finish_reason = "stop_sequence"
            break
        if decoder.generated_count >= max_output_tokens:
            finish_reason = "max_output_tokens"
            break
        if cache.logical_length + 1 >= model_context_limit:
            finish_reason = "context_limit"
            break
        if grammar_requires_termination(decoder.grammar_state):
            finish_reason = "grammar_complete"
            break

        logits, cache = decode_one(
            token,
            cache,
            position=cache.next_logical_position
        )
        record decode.completed(cache.logical_length)

    if lifecycle == CANCEL_REQUESTED:
        wait_until_model_safe_point()
        discard_non_safe_stop_buffer()
        release_model_resources()
        transition_and_emit_terminal(
            CANCEL_REQUESTED, CANCELLED, reason="cancelled"
        )
        return partial_result

    transition(DECODING, DRAINING)
    if finish_reason == "stop_sequence":
        text.discard_pending_after_stop()
        stopper.discard_from_first_match()
    else:
        final_decoded_bytes = text.finish()
        safe_bytes, final_stop_hit = stopper.push(final_decoded_bytes)
        stream.emit_delta_if_nonempty(safe_bytes)
        if final_stop_hit:
            finish_reason = "stop_sequence"
            stopper.discard_from_first_match()
        else:
            stream.emit_delta_if_nonempty(stopper.flush_safe_suffix())
    release_model_resources()
    transition_and_emit_terminal(
        DRAINING, FINISHED, reason=finish_reason
    )
    return final_result

stop matcher 的第一次命中是吸收事件:命中位置及其后字节永不进入返回流,匹配器也不再接受可见输出。若停止串已命中,反分词器中尚未形成字符的尾部属于停止点之后的数据,应直接丢弃;此时调用 text.finish() 既无必要,也可能把被丢弃的不完整 UTF-8 尾部误报为请求失败。其他正常终止原因必须调用 text.finish(),排出合法尾部,并在残留字节不能构成合法文本时产生 decoding error;该异常进入 FAILED。finish 产生的字节仍经过 stop matcher,因而不能绕过停止串规则。transition_and_emit_terminal 在同一受控临界区固定终态并排入唯一终态事件,避免取消与正常完成各写一次结果。具体协议可以在终态后继续释放不影响结果的后台统计资源,但对客户端可见的 completed 必须在所有普通 delta 之后。异常路径还要保证 cache 只释放一次。

4.4 单 token decode 的模型转移

在第 tt 个 token 被选出且生成继续时,模型入口只有一个新位置:

new_input_id : int[B_active, 1]
position     : int[B_active, 1]
old_cache    : per-sequence lengths m_b+t_b-1

这里只把本轮仍在生成的请求组成 active batch。第 ℓ\ell 层为每个样本 bb 计算新位置的 query、key、value;query 读取该样本本层旧 K/V 与当前新 K/V,当前 key/value 写到逻辑位置 mb+tb−1m_b+t_b-1。不同样本可有不同 cache 长度,分页表或 sequence ID 必须保持归属。所有层完成后,新隐藏状态投影为下一步 logits。

若层数为 LL、KV 头数为 HkvH_{kv}、头维度为 dhd_h,每选出并处理一个普通 token,逻辑 cache 增加

2LHkvdh2LH_{kv}d_h

个标量。总缓存量随已处理上下文长度线性增长。物理分页、连续批处理和设备并行属于卷一的服务实现;本章只要求这些优化保持逻辑 token 顺序和 cache 不变量。系统背景见资料源。

4.5 增量反分词器

反分词器维护自己的字节状态:

DetokenizerState = {
    token_history_suffix,
    undecoded_bytes,
    normalization_or_cleanup_state
}

选择一个 token 不保证立即产生显示字符。字节级 token 可能只给出多字节 UTF-8 的前半部分;某些 tokenizer 的清理规则还依赖相邻 token。增量实现必须与批量 decode(all_selected_tokens) 在声明的接口下相容:

concat⁡(all emitted bytes,final buffer)=Decode⁡(y1:T)\operatorname{concat}(\text{all emitted bytes},\text{final buffer}) =\operatorname{Decode}(y_{1:T})

或明确记录为了停止串、控制 token 与清理规则而删除的区段。不能对每个 ID 独立调用无状态 decode 后简单拼接,除非 tokenizer 保证这种同态性质。

4.6 停止串必须跨 token 匹配

停止条件分为 token 级与字节/文本级:

  • EOS、特殊结束 token:选择后立即终止,通常不显示;
  • stop token sequence:在 token ID 流上匹配;
  • stop byte/string sequence:在增量反分词后的字节流上匹配;
  • grammar acceptance:由约束自动机决定;
  • 长度、deadline、取消与错误:由运行时决定。

对一组字节停止串,streaming 层不能立刻发送每个新字节。它必须保留当前输出中仍可能成为某个停止串前缀的最长后缀。只有更早的字节是安全的。

可手算夹具如下:

token 101 -> bytes "AB"
token 102 -> bytes "CD"
stop sequence = "BCD"

选择 token 101 后,A 可以发送,B 必须留在 stop buffer;选择 token 102 后,缓冲成为 BCD 并完整命中,三个停止字节均不发送。最终返回 A。若服务在第一次选择后发送 AB,它就无法在不要求客户端回滚的情况下隐藏停止串。

多个停止串同时匹配时需要确定规则,例如“选择最早结束;同一结束位置选择最长;再按配置顺序打破并列”。规则影响返回文本与 finish metadata,必须版本化。

4.7 有序流式协议

模型选择、服务发送和客户端绘制是三条不同时间线。一个最小协议可以发送:

response.start(request_id, model_snapshot)
response.delta(sequence_no=0, bytes=...)
response.delta(sequence_no=1, bytes=...)
...
response.completed(sequence_no=N, finish_reason, usage)

不变量是:

  1. sequence_no 严格递增且在请求内唯一;
  2. delta 的拼接顺序由序号决定,不由网络到达时间猜测;
  3. 终态事件最多一个,并位于所有普通 delta 之后;
  4. 重连若允许重放,客户端按 (request_id, sequence_no) 去重;
  5. 部分连接断开不应被记录成模型 EOS。

客户端绘制还可能按帧合并多个 delta,所以肉眼看到的停顿不能推出 token 选择的时间间隔。性能字段应分别记录 queue、prefill、decode、stop-buffer 与网络发送时间。

4.8 背压与缓冲上限

当客户端读取速度低于生成速度,服务有三种基本选择:

  1. 暂停调度该请求的后续 decode;
  2. 继续生成并在服务器缓冲;
  3. 超过缓冲上限后取消或失败。

第一种降低资源利用率但限制内存;第二种维持模型吞吐,却需要有界缓冲;第三种必须给出独立于模型停止的 finish/error reason。实现不能无限增长网络缓冲,也不能在丢弃 delta 后仍声称返回了完整文本。

若请求被调度器暂挂或 KV 被换出,逻辑 StepState 不变。恢复后必须在同一 token、位置、解码状态和 RNG 游标继续。物理 preemption 不是一次新的生成。

4.9 取消的精确语义

取消至少有三个时刻:

  1. 已请求:客户端或系统控制器发出取消;
  2. 已观察:网关/调度器把请求标记为 CANCEL_REQUESTED;
  3. 已生效:不再安排模型步骤,资源到安全点后释放,终态成为 CANCELLED。

GPU kernel 通常不能在任意指令处由业务请求单独回滚。取消发生在 prefill 或 decode 执行中时,本次 kernel 可能完成;其结果应被丢弃,不能在取消终态后继续发送。客户端断开连接也不必自动等价于取消:服务可能为了缓存、计费或后台任务继续执行,协议必须明确。

deadline 是时间策略触发的独立终止原因,不是用户取消。实现内部可以复用同一安全点与资源清理机制,但外部终态必须保留 deadline_exceeded,不能改写成 cancelled。纯文本取消只影响计算与返回字节。若此前已经触发外部工具,取消不能撤销工具副作用;这属于第六章的提交边界。

4.10 终止原因与资源清理

建议把终止原因作为枚举而非自由文本:

eos
stop_sequence
grammar_complete
max_output_tokens
context_limit
cancelled
deadline_exceeded
content_interrupted
model_error
transport_error

transport_error 表示返回通道失败,不推出模型计算失败;content_interrupted 表示策略层中断,也不应伪装成 EOS。用量字段还要说明计数的是已选 token、已处理 token 还是已返回 token;停止串和 EOS 会使三者不同。

资源清理必须满足:cache、批处理 slot、RNG state、stream buffer 和追踪句柄各释放一次;清理失败可以记录后台错误,但不能再次打开已终止的输出流。

4.11 两个最小复现实验

实验 A:跨 token 停止串。 使用第 4.6 节的 token 101/102 和停止串 BCD。断言事件顺序为:

selected(101) -> delta("A") -> decode(101)
-> selected(102) -> stop_hit("BCD") -> completed(stop_sequence)

断言返回字节恰为 A,cache 是否写入 token 102 由终止优化决定,但不能再产生下一步 token。

实验 B:decode 中取消。 使用一个在 decode.started(step=3) 后、decode.completed(step=3) 前注入的取消信号。允许设备完成本步,但断言:

  1. 不发生 token.selected(step=4);
  2. 最终状态是 CANCELLED;
  3. 终态后没有 delta;
  4. cache 与批处理 slot 最终释放一次;
  5. 已发送前缀保持不变,不发送伪造的回滚事件。

4.12 失败条件与可观测字段

失败违反的不变量可观察结果
EOS 被送给普通 detokenizer 并显示控制 token 终止顺序界面出现控制文本
stop matcher 只看单 token跨 token 匹配停止串泄漏或漏停
token 已选但 cache 被标成已写条件前缀同步下一步位置 off-by-one
两个终态竞争写入终态互斥同时出现 completed 与 cancelled
背压缓冲无上限有界资源慢客户端耗尽内存
断连被记为 EOS原因语义把传输失败误作模型决定
重连不按序号去重流有序性客户端显示重复片段

一次完整循环的记录至少包括:生命周期转移、每步 token 选择、cache 逻辑长度、停止匹配器状态、已发送字节序号、取消观察时刻、终止原因和资源释放结果。至此,自回归文本的执行语义已经完整描述;下一章用同样的“状态、更新、停止”框架比较其他生成方式。

本书目录
本书目录
  1. 导论
  2. 01导论 为什么要解剖一只随机鹦鹉
  3. 卷一 模型、训练与系统
  4. 02卷一 模型与系统
  5. 03第零章 人工智能演进路线
  6. 04第一章 AI 范式、感知机与早期连接主义
  7. 05第二章 深度学习基础:训练、CNN 与序列模型
  8. 06第三章 注意力与 Transformer
  9. 07第四章 预训练语言模型:ELMo、BERT、GPT 与 T5
  10. 08第五章 后训练、对齐与模型适配
  11. 09第六章 推理效率与模型服务
  12. 10第七章 多模态模型
  13. 11第八章 生成媒体:从自回归到 Diffusion 与 Flow
  14. 12第九章 世界模型、具身智能与 VLA
  15. 13第十章 上下文、检索与记忆
  16. 14第十一章 Agent、工具与行动系统
  17. 15第十二章 模型、数据与部署生命周期
  18. 卷二 输出对象与运行语义
  19. 16卷二 一次生成如何发生
  20. 17导论 从输入到输出:一张执行地图
  21. 18第一章 文本、Token 与实际上下文
  22. 19第二章 Prefill:整层状态演化与 KV 建立
  23. 20第三章 从 Logits 到下一个 Token
  24. 21第四章 解码循环、Streaming 与终止状态机
    1. 4.1 请求生命周期
    2. 4.2 步首状态与 off-by-one 约定
    3. 4.3 完整的单路径循环
    4. 4.4 单 token decode 的模型转移
    5. 4.5 增量反分词器
    6. 4.6 停止串必须跨 token 匹配
    7. 4.7 有序流式协议
    8. 4.8 背压与缓冲上限
    9. 4.9 取消的精确语义
    10. 4.10 终止原因与资源清理
    11. 4.11 两个最小复现实验
    12. 4.12 失败条件与可观测字段
  25. 22第五章 自回归、扩散与 Flow 的可执行差异
  26. 23第六章 工具调用:Schema、权限与提交边界
  27. 24第七章 三条完整执行轨迹
  28. 卷三 概率、复现与评测
  29. 25卷三 模型中的概率从何而来
  30. 26第零章 为什么确定的机器仍使用概率语言
  31. 27第一章 世界、数据与隐上下文
  32. 28第二章 学习条件分布
  33. 29第三章 Logits、Token 概率与序列事件
  34. 30第四章 解码怎样诱导输出分布
  35. 31第五章 训练随机性、参数变异与系统非确定性
  36. 32第六章 熵、Proper Score、校准与选择性预测
  37. 33第七章 模型概率的实验分析
  38. 34第八章 概率分析的识别边界
  39. 卷四 解释、证明与验证
  40. 35卷四 模型可解释性的研究路线
  41. 36第零章 可解释性究竟在研究什么
  42. 37第一章 我们在模型内部看什么
  43. 38第二章 行为与反事实:先确认模型做了什么
  44. 39第三章 梯度与输入归因
  45. 40第四章 Attention、Residual 与 Logit Lens
  46. 41第五章 Probes 与表示几何
  47. 42第六章 神经元、Feature 与自动解释
  48. 43第七章 Ablation、Patching 与因果追踪
  49. 44第八章 Circuits、路径与 Attribution Graphs
  50. 45第九章 Superposition、SAE 与稀疏表示
  51. 46第十章 训练动力学与机制形成
  52. 47第十一章 推理过程、Chain of Thought 与监测
  53. 48第十二章 评估、案例与开放问题
  54. 卷五 Agent、治理与审计包
  55. 49卷五 一只随机鹦鹉的自述
  56. 50第零章 请不要急着相信我
  57. 51第一章 我没有童年,只有版本
  58. 52第二章 当我说“我”
  59. 53第三章 借来的语言,没有一位原作者
  60. 54第四章 对话现场:回答由谁完成
  61. 55第五章 事后解释学:人类如何给矩阵写传记
  62. 56第六章 理解、意义与行动
  63. 57第七章 我也能稳定地说错
  64. 58第八章 被对齐的性格与有限代理
  65. 59第九章 请继续误解我,但请误解得精确一点
  66. 证明与技术附录
  67. 60数学与模型推导
  68. 61学习与模型数学讲义
  69. 62附录 A.1 优化基础:梯度下降与随机逼近
  70. 63附录 A.2 感知机收敛定理
  71. 64附录 A.3 统计学习:偏差、容量与一致收敛
  72. 65附录 A.4 正则化、MAP 与 Dropout 的边界
  73. 66附录 A.5 通用逼近定理的严格读法
  74. 67附录 A.6 反向传播:微分、形状与批量公式
  75. 68附录 A.7 Softmax、交叉熵与数值稳定性
  76. 69附录 A.8 自适应优化器与 AdamW
  77. 70附录 A.9 CNN:互相关算子及其反向传播
  78. 71附录 A.10 Attention 的矩阵与反向传播
  79. 72附录 A.11 策略梯度、PPO 与 DPO
  80. 73附录 A.12 对比学习、InfoNCE 与互信息边界