priority、routing key 与 trace header 如何随请求传播#

这章解决什么问题#

前面的 request lifecycle 已经覆盖了生成主线、非生成与控制请求分叉、多 worker 入口和首尾 manager 闭环,但还缺一个非常工程化的问题:除了 prompt、token、tool 参数这些显眼内容之外,请求身上那些“附加元数据”到底怎样一路传播?

这里说的元数据包括:

  • priority
  • routing_key
  • custom_labels
  • external_trace_header
  • received_time
  • routed_dp_rank

这一章的目标,就是把这些信号从 HTTP header / protocol field 一直到 runtime request object 的传播路径讲清楚。

为什么这层值得单独成章#

因为很多真实工程问题都不在“文本内容”本身,而在这些附加信号上:

  • 请求为什么被排到某个优先级
  • 为什么被送到某个 DP rank
  • 为什么 /v1/loads 里某个 routing key 聚成一团
  • 为什么 trace 能把同一次外部请求串起来
  • 为什么 exporter / metrics 带上了某组 custom label

如果不把这层讲出来,request lifecycle 仍然会过于“只看 payload”,不够像真实系统。

一张图:请求除了 payload,还带着一串控制与观测信号一起流动#

这张图解决的理解障碍是:很多读者默认只有 prompt / sampling params 会进入 runtime,但实际还有一整串 side-channel metadata 也在同行。

flowchart LR
    Header["HTTP headers / request fields"] --> Extract["serving_base extract_*"]
    Extract --> Obj["GenerateReqInput / EmbeddingReqInput"]
    Obj --> TM["TokenizerManager"]
    TM --> SCH["Scheduler / DP controller / metrics / tracing"]

这张图比纯文字多解释的一点是:这些信号不是后补丁,而是跟请求对象一起正式进入 runtime。

serving_base.py 里的三个 extract 函数为什么关键#

这几个函数几乎定义了外部信号怎样进入系统:

  • extract_custom_labels(raw_request)
  • extract_routing_key(raw_request)
  • extract_routed_dp_rank_from_header(raw_request, body_routed_dp_rank)

它们说明一个重要设计事实:对于 OpenAI-compatible surface,某些 runtime 控制信号并不只来自请求体,也可能来自 HTTP header。这让系统可以和更外层网关、router、代理或 observability 平台协作。

received_time 为什么值得特别提#

OpenAIServingBase.handle_request(...) 会在入口先记录 received_time,然后把它塞进 adapted_request。这说明系统区分:

  • 请求到达 API 表面的时刻
  • 请求真正进入 tokenizer / scheduler 的时刻

这和前面写过的 ReqTimeStats 是自然回扣。一本更厚的系统书,应该把这种“时间语义的源头”讲出来,而不是只在后面的观测章节里突然出现。

priority 从哪里来,为什么要这么早就落进对象#

ChatCompletionRequestCompletionRequestEmbeddingRequest 等协议对象本身就允许带 priority。到了 _convert_to_internal_request(...),它会被直接写进:

  • GenerateReqInput.priority
  • EmbeddingReqInput.priority

再往后,TokenizerManager 还会根据 server 配置给它补默认值。这说明优先级不是 scheduler 临时猜出来的,而是从请求对象进入系统时就已经有机会被明确携带。

routing_key 的传播为什么同时服务调度与观测#

extract_routing_key(raw_request) 默认从 x-smg-routing-key header 取值,然后写进请求对象。后面:

  • schedule_policy.py 会在 waiting queue 重排时使用它
  • metrics / /v1/loads 会统计它的分布

也就是说,这不是一个只服务某一章的字段,而是典型的“跨层传播元数据”:

  • 调度层消费它
  • 观测层也消费它

这类字段最适合放在 request lifecycle 里先讲清传播路径。

custom_labels 为什么是 request-level 观测信号,而不是日志私货#

extract_custom_labels(raw_request) 会读取配置指定的 header,再筛成允许的 label。后面这些 label 会被带到 metrics/exporter 路径里。

这说明 custom labels 在设计上不是“日志里临时附一点信息”,而是请求级观测维度的一部分。它也解释了为什么这类字段必须先挂在 request object 上,而不是在 exporter 最后再去 HTTP 上下文里翻。

external_trace_header 的传播怎样和 tracing 接起来#

io_struct.py 里明确给 GenerateReqInput / EmbeddingReqInput 留了 external_trace_headerreceived_timeTokenizerManager 初始化 request time stats 时,会优先使用对象上的 external_trace_header,否则才从 HTTP request headers 里提取。

这说明 trace context 的传播不是零散实现,而是有显式对象字段承载的。它非常值得写进书里,因为这直接决定跨系统 trace 能否串起来。

routed_dp_rank 是元数据里最“强”的一个#

routing_key 这种软信号不同,routed_dp_rank 更接近硬指令:

  • 可以从 body 来
  • 也可以由 header 覆盖 body
  • 后面会被 DataParallelController 直接用于路由

把它和其他元数据一起看,能帮助读者建立一层更精细的区分:

  • 有些字段是观测信号
  • 有些字段是调度 hint
  • 有些字段是强路由命令

这一层最容易出现的误判#

1. 把这些字段都当成“只是日志元数据”#

实际上有的会影响调度,有的会影响 tracing,有的会影响 placement。

2. 以为所有信号都只来自请求体#

header 也是重要输入面,尤其是 routing 与 tracing。

3. 以为这些字段在后面才临时拼接#

很多字段一开始就已经进入 GenerateReqInput / EmbeddingReqInput

如果你怀疑问题在“请求为什么被这样对待”,先怎么查#

建议按这个顺序:

  1. 先看协议对象或 header 是否真的带了 priority / routing_key / routed_dp_rank / custom labels。
  2. 再看 _convert_to_internal_request(...) 是否把这些值正确写进内部对象。
  3. 再看 TokenizerManager 是否补了默认值或附加了 trace header。
  4. 最后再回到调度、观测或 DP 路由章节看这些信号如何被消费。

小结#

这一章真正想补齐的,是 request lifecycle 里经常被忽略的一面:

  • 请求进入系统时,不只带文本和 token
  • 还带着 priority、routing、trace、labels 这些控制与观测信号
  • 它们的传播路径一旦看清,后面很多“为什么系统这样处理这条请求”的问题就更容易解释

到这里,请求生命周期就不只覆盖数据载荷,也开始覆盖伴随请求一起传播的控制与观测语义。