Token的一生:调度器详解"

admin 2026-10-21 05:03:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

Token 的一生:调度器详解

原创

zouyee
zouyee

DCOS

2026年8月19日 08:00
上海

在小说阅读器读本章

去阅读

系列定位:中级。理解 SGLang 调度器如何分配 Token 预算、控制并发,以及 Chunked Prefill 机制。

Summary

本篇解析调度器的核心决策逻辑。内容涵盖:Token 预算的三个维度(rem_total_tokens、rem_chunk_tokens、rem_input_tokens);PrefillAdder.add_one_req 的准入控制流程与 AddReqResult 枚举;Chunked Prefill 如何把长 prompt 切块以降低短请求的 TTFT;extend_range 精确控制每轮计算范围;Decode 阶段的批量打包;请求结束与 KV Cache 释放;以及 fcfs/lpm/dfs-weight 等调度策略的适用场景。附录预览了 DFlash 投机解码、DSpark 半自回归草稿、DSA 稀疏注意力三大进阶机制。


1. 调度器面临的核心问题

GPU 的显存是有限的,KV Cache 占用大量显存。调度器需要在每个推理步(forward pass)前回答:

这一步能跑哪些请求?各跑多少 token?

如果贪心地把所有等待请求都塞进去,显存会爆(OOM);如果过于保守,GPU 利用率低,吞吐浪费。

SGLang 的调度策略核心是 Token 预算(Token Budget)。


2. Token 预算的三个维度

schedule_policy.py 里,PrefillAdder(负责本轮 prefill 准入的核心类)维护三个预算计数器:

# python/sglang/srt/managers/schedule_policy.py(精简,真实类名为 PrefillAdder)
class PrefillAdder:
    def __init__(self, ..., rem_input_tokens: int, rem_chunk_tokens: Optional[int], ...):
        self.rem_input_tokens   # 本轮 prefill 阶段输入 token 上限
        self.rem_chunk_tokens   # 本轮 chunked prefill 可用的 token 数(Optional[int])

    @property
    def rem_total_tokens(self):  # 注意:这是一个 @property,动态计算
        # 返回 KV Pool 可用+可驱逐容量 - 已预留偏移量
        ...
预算 含义 典型来源
rem_total_tokens KV Cache 中还剩多少槽位(@property,动态查询) 显存大小 / KV block 大小
rem_chunk_tokens 本轮 chunked prefill 的 token 配额 --chunked-prefill-size
rem_input_tokens 本轮最多处理多少输入 token --max-prefill-tokens

3. 请求准入:add\_one\_req

每次调度循环,SchedulePolicy.add_one_req() 决定一个等待中的请求能不能进入本轮:

# 简化示意(非完整代码,省略了边界检查和 LoRA/EOS 等细节)
def add_one_req(self, req, has_chunked_req, truncation_align_size):
    max_new = min(req.sampling_params.max_new_tokens, CLIP_MAX_NEW_TOKENS)
    cand_extend_input_len = len(req.full_untruncated_fill_ids) - len(req.prefix_indices)
    total_tokens = cand_extend_input_len + max_new + self.page_size

    if total_tokens >= self.rem_total_tokens:
        return AddReqResult.NO_TOKEN

    input_tokens = self.ceil_paged_tokens(cand_extend_input_len)

    # 无 chunked prefill(rem_chunk_tokens is None)时:
    # 若已有其他请求在本轮,则检查 prefill token 上限
    if self.rem_chunk_tokens is None:
        if len(self.can_run_list) != 0 and input_tokens >= self.rem_input_tokens:
            return AddReqResult.OTHER
        # 整个请求一次处理完
        req.set_extend_range(len(req.prefix_indices), len(req.full_untruncated_fill_ids))
        self.can_run_list.append(req)
    elif input_tokens <= self.rem_chunk_tokens:
        # 有 chunked prefill 且本轮配额够:一次处理完
        req.set_extend_range(len(req.prefix_indices), len(req.full_untruncated_fill_ids))
        self.can_run_list.append(req)
    else:
        # 有 chunked prefill 且配额不够:只处理一块
        trunc_len = self.rem_chunk_tokens // self.page_size * self.page_size
        if trunc_len <= 0:
            return AddReqResult.OTHER
        req.set_extend_range(len(req.prefix_indices), len(req.prefix_indices) + trunc_len)
        self.can_run_list.append(req)
        self.new_chunked_req = req

    return self.budget_state()  # 返回 CONTINUE / NO_TOKEN / OTHER

AddReqResult 有三种结果:

结果 含义
NO_TOKEN KV Cache 满了,请求必须等待
OTHER 本轮 prefill 配额用完,请求推迟
CONTINUE 进入本轮,继续追加更多请求

枚举定义见 schedule_policy.py 第 422 行:class AddReqResult(Enum): CONTINUE / NO_TOKEN / OTHER


4. Chunked Prefill(分块预填充)

问题背景

假设有两个请求同时到达:

  • 请求 A:prompt 长度 = 10,000 tokens(长文档摘要)
  • 请求 B:prompt 长度 = 10 tokens(简单问答)

如果按序处理,请求 A 的 prefill(处理全部 10,000 tokens)会独占 GPU 很长时间,导致请求 B 的\\首 token 延迟(TTFT)\\极高。

Chunked Prefill 的解法

把长请求的 prefill 切成小块,和 decode 请求混合执行:

不使用 Chunked Prefill:
  Round 1: [A prefill: 10000 tokens]           ← B 等待
  Round 2: [A decode: 1 token] [B prefill: 10 tokens] [B decode: 1 token]

使用 Chunked Prefill(chunk_size=512):
  Round 1: [A prefill chunk 1: 512] [B prefill: 10] [running decodes]
  Round 2: [A prefill chunk 2: 512] [running decodes]
  ...
  Round 20: [A prefill chunk 20: 512] [A decode: 1] [running decodes]

请求 B 的 TTFT 从\"等 A 跑完 prefill\"变成\"等一个 chunk\",大幅降低。

代码位置

# python/sglang/srt/managers/scheduler.py
def init_chunked_prefill(self):
    self.chunked_prefill_size = self.server_args.chunked_prefill_size
    # chunked_prefill_size=None 表示禁用
    # chunked_prefill_size=512  表示每轮最多处理 512 个 prefill tokens
    self.chunked_req = None  # 当前正在分块处理的请求

# 在每轮调度中:
if self.chunked_req is not None:
    # 优先继续上一轮未完成的分块请求
    schedule_policy.rem_chunk_tokens = self.chunked_prefill_size
    # 把 chunked_req 排在队列最前面

extend\_range:精确控制本轮计算范围

Req.extend_range 是 Range(start, end),告诉 TP Worker 本轮只需要计算哪段 tokens 的注意力:

origin_input_ids: [t0, t1, t2, t3, t4, t5, t6, t7, t8, t9]
prefix_indices (cached): [t0, t1, t2]   ← 已在 KV Cache 里,不用算

第一轮 extend_range = Range(3, 7)   → 计算 t3, t4, t5, t6
第二轮 extend_range = Range(7, 10)  → 计算 t7, t8, t9(完成 prefill)
第三轮 extend_range = ...           → decode 阶段,每轮只算 1 个新 token

5. Decode 阶段的调度

Decode 阶段每轮只生成 1 个新 token(或在投机解码下生成多个),相比 prefill 计算量小得多。调度器把所有正在 decode 的请求打包成一个批次(batching),一次 forward pass 同时处理多个请求,充分利用 GPU 并行性。

Decode 批次(假设 4 个请求同时在 decode 阶段):
  req_001: origin=[...] + output=[t_out_0, t_out_1, t_out_2]  → 预测 t_out_3
  req_002: origin=[...] + output=[t_out_0]                     → 预测 t_out_1
  req_003: origin=[...] + output=[t_out_0, ..., t_out_99]     → 预测 t_out_100
  req_004: origin=[...] + output=[]                            → 预测第一个输出 token

一次 forward pass,同时为 4 个请求各生成 1 个新 token

6. 请求结束与 KV Cache 释放

当请求满足停止条件(达到 max_new_tokens、生成了 EOS、命中 stop_str),finished_reason 被设置:

class Req:
    finished_reason: Optional[BaseFinishReason]
    # 子类(定义在 schedule_batch.py):
    #   FINISH_LENGTH           — 达到 max_new_tokens
    #   FINISH_MATCHED_TOKEN    — 生成了 stop_token_id
    #   FINISH_MATCHED_STR      — 文本命中 stop 字符串
    #   FINISHED_MATCHED_REGEX  — 文本命中 stop_regex(注意:源码里是 FINISHED_ 而非 FINISH_)
    #   FINISH_ABORT            — 客户端断开或内部错误

请求结束后,调度器释放它在 KV Cache 里独占的槽位,空出来给下一个请求。已计算过的共享前缀(例如 system prompt)则留在 Radix Cache 里,供后续请求复用。


7. 调度策略

SGLang 支持多种调度策略,通过 --schedule-policy 指定:

策略 类别 选请求的方式 适用场景
fcfs 缓存无关 先来先服务(默认) 通用
lof 缓存无关 最长输出优先(longest output first) 长输出请求批次
random 缓存无关 随机 测试/基准
routing-key 缓存无关 按 routing\_key 路由到同一 worker 多租户隔离
lpm 缓存感知 最长前缀匹配优先 重复前缀多(RAG、Agent)
dfs-weight 缓存感知 深度优先 + 权重 树状请求(beam search、tree of thought)

优先级调度(按 request.priority 字段排序)不是独立的 --schedule-policy 值,而是通过 --enable-priority-scheduling 标志叠加在 fcfs 或 lof 之上启用:--schedule-policy fcfs --enable-priority-scheduling。

LPM 策略和 Radix Cache 配合效果最好:同一个 system prompt 的请求被优先调度在一起,KV Cache 命中率最高。


小结

每轮调度循环:
  ① 检查 Radix Cache,更新各请求的 prefix_indices
  ② 遍历等待队列,用 add_one_req 做准入控制:
     - 检查 rem_total_tokens(显存)
     - 检查 rem_chunk_tokens(分块配额)
  ③ 构建 ScheduleBatch:
     - 正在 decode 的请求(全部)
     - 可以 prefill 的请求(受预算限制)
  ④ 发给 TP Worker 执行 forward pass
  ⑤ 收结果,更新 output_ids,检查终止条件,释放显存

下一篇:Radix Cache 是 SGLang 高效复用 Token 计算的核心,我们来看它是怎么用前缀树管理 KV Cache 的。


附录:Token 生成加速的三大进阶机制

投机解码(Speculative Decoding)背景

标准 decode 每轮只生成 1 个 token,GPU 是 memory-bound 而非 compute-bound——大量时间花在搬运 KV Cache 权重而非真正计算上。投机解码的核心思路:

传统 decode:
  Round 1 → 1 token(KV 读 1 次)
  Round 2 → 1 token(KV 读 1 次)  ← 大量时间在等内存

投机解码:
  草稿阶段:小模型快速生成 N 个候选 token(廉价)
  验证阶段:大模型一次 forward 验证所有候选(贵但值)
  → 每次大模型 forward 实际产出 ~k 个 token(k ≤ N)
  → 吞吐量提升正比于平均接受长度 k

投机解码不改变输出分布(lossless),生成质量与标准 decode 完全等价。


DFlash:混合注意力草稿模型

DFlash 是 SGLang 已实现的投机解码方案(sglang/srt/speculative/dflash_worker_v2.py),草稿模型架构设计为全注意力层与滑窗注意力层交替排列:

DFlash 草稿模型层结构:
  Layer 0: sliding_window_attention(只看最近 W 个 token,KV 开销小)
  Layer 1: full_attention(看全部上下文,精度高)
  Layer 2: sliding_window_attention
  Layer 3: full_attention
  ...

滑窗层降低草稿阶段的 KV 读写量,全注意力层保证草稿质量。调度器对 DFlash 请求的处理分两步:

# 草稿阶段:
#   draft_model.forward(input_ids) → draft_tokens [batch, N]
#   构建 DFlashVerifyInput(draft_token=..., draft_token_num=N)

# 验证阶段(ForwardMode.TARGET_VERIFY):
#   target_model.forward(draft_tokens) → logits [batch, N, vocab_size]
#   tree_speculative_sampling_target_only() → 接受/拒绝每个 draft token
#   accepted_tokens → 追加到 req.output_ids

相关源码:dflash_worker_v2.py / dflash_info.py / models/dflash.py


DSpark:置信度调度的半自回归投机解码

DSpark(arxiv:2606.19348,DeepSeek + 北京大学,2026-06-27)是比 DFlash 更新的投机解码方案,在 acceptance length 上比 DFlash 高出 16.3%–30.9%。

核心创新 1:半自回归草稿生成

传统投机解码的草稿生成要么完全并行(精度差,后续 token 与前驱脱耦),要么完全自回归(速度慢)。DSpark 引入混合架构:

并行骨干网络(Parallel Backbone)
  ↓ 一次 forward 同时生成 block 内所有 draft tokens(快)
  ↓
轻量顺序模块(Sequential Module / Markov Head)
  ↓ 在 block 内引入相邻 token 的依赖关系(防止\"后缀衰减\")
  ↓
Block 内 draft tokens 质量显著高于纯并行方案

\"后缀衰减(suffix decay)\"指的是:纯并行草稿生成时,block 尾部的 token 因为看不到前驱 token 的预测结果,精度会随位置递减。轻量顺序模块以极小代价修复这个问题。

核心创新 2:置信度调度验证(Confidence-Scheduled Verification)

不同请求的草稿接受率不同。DSpark 根据\"前缀存活概率\"估算每个请求在当前轮的预期接受长度,动态决定草稿块大小:

高置信度请求(模型对上下文很\"笃定\")→ 发较长草稿,高接受率
低置信度请求(分叉较多,如创意写作) → 发较短草稿,减少验证浪费

这避免了固定草稿长度带来的两种浪费:对难请求生成太长草稿、对易请求生成太短草稿。

关键指标(DeepSeek 官方测试)

指标 V4-Flash V4-Pro
单用户生成速度提升 60–85% 57–78%
系统总吞吐提升 51–400% —
vs DFlash acceptance length +16.3–30.9% —

当前 SGLang 支持状态

DSpark 论文于 2026-06-27 发布,SGLang 主干尚未合并 DSpark 支持(截至本文写作时)。DFlash 是当前可用的 SGLang 原生投机解码方案。如需跟踪 DSpark 集成进展,可关注 SGLang 仓库的 speculative/ 目录和相关 Issue。


DSA(Dynamic Sparse Attention):稀疏 KV 访问

上面两种方案解决的是\"如何减少大模型 forward 次数\",DSA 解决的是\"每次 forward 里如何减少 KV 读取量\"。

对于超长上下文(1M tokens),每个 decode 步都读全部 KV Cache 带宽压力极大。DSA 在注意力计算时只访问 top-k 个重要 KV 块:

标准 decode attention:读取 seqlen × kv_heads × head_dim 的全部 KV
DSA decode attention:index → top-k 重要 KV 块(k << seqlen)→ 只读这些块

核心组件在 sglang/srt/layers/attention/dsa/:

  • dsa_indexer.py:维护每个请求的 KV 重要性索引,动态更新
  • dsa_topk_backend.py:基于 top-k 索引的稀疏注意力 kernel

三者关系:Radix Cache 在请求间共享前缀 KV(减少重复计算);DFlash/DSpark 在时间维度批量验证多个 token(减少 forward 次数);DSA 在空间维度稀疏访问 KV(减少单次 forward 带宽)。三者正交,可以叠加启用。


小结

每轮调度循环(完整版):
  ① Radix Cache 前缀匹配,更新 prefix_indices
  ② add_one_req 做 Token 预算准入控制
  ③ 构建 ScheduleBatch:
     - prefill 请求(受 chunked prefill 分块)
     - decode 请求(全部打包,含投机解码)
  ④ TP Worker forward pass:
     - DFlash 请求:draft forward → verify forward(TARGET_VERIFY 模式)
     - DSA 请求:decode forward 只访问 top-k KV 块
  ⑤ 收结果,更新 output_ids,检查终止条件,释放显存

下一篇:Radix Cache 是 SGLang 高效复用 Token 计算的核心,我们来看它是怎么用前缀树管理 KV Cache 的。


Token的一生:调度器详解" 网络安全文章

Token的一生:调度器详解"

本文解析SGLang调度器核心决策逻辑,包括Token预算的三个维度(rem_total_tokens、rem_chunk_tokens、rem_input_tokens)、PrefillAdder的请求准入控制与AddReqResult枚举、ChunkedPrefill将长prompt切块以降低短请求TTFT的机制、extend\\_range精确控制计算范围、Decode阶段批量打包、请求结束与KVCache释放,以及fcfs/lpm/dfs-weight等调度策略的适用场景。
CU-Xlab大网僵尸网络分布式传播活动分析报告" 网络安全文章

CU-Xlab大网僵尸网络分布式传播活动分析报告"

本报告基于运营商大网流量分析指出,僵尸网络攻击已演变为低频慢扫、长周期巡航与隐蔽C2控制相结合的自动化传播体系。攻击源以美荷等国为主,重点针对Telnet、SSH及IoT等高危端口实施弱口令爆破与漏洞利用,形成从侦查到横向扩散的完整闭环。建议企业加强暴露资产治理,关闭非必要高危服务并清理弱口令,部署基于流量建模的低频扫描与C2心跳检测规则,结合威胁情报联动处置,重点关注凌晨攻击高峰以阻断传播链路。
安永(EY)披露一起数据泄露事件" 网络安全文章

安永(EY)披露一起数据泄露事件"

安永(EY)于2026年7月17日披露数据泄露事件,其第三方技术支持工单系统在3月28日至4月12日期间被攻陷,可能泄露客户税务机密信息。事件于4月23日被发现,目前无勒索组织声称负责。建议企业加强第三方供应链安全管理。
日本快递企业佐川急便泄露7万用户隐私" 网络安全文章

日本快递企业佐川急便泄露7万用户隐私"

日本佐川急便因系统修复失误导致约7万会员姓名与邮箱等信息错发泄露,非网络攻击且不含财务数据。该事件与近期日本多起大型泄露案类似,暴露出企业系统变更管控短板。建议组织强化系统更新测试与外包安全审计,从技术与管理双维度防范内部操作失误引发的数据泄露风险。
评论:0   参与:  0