NNRP/1-preview2 协议设计
1. 定位
NNRP 在本文中的正式全称是 Neural Network Runtime Protocol,即“神经网络运行时协议”。NNRP/1-preview2 是在 NNRP/1-preview1 之后的第二份预览阶段设计文档。它不是另一条独立的大版本线,而是在 NNRP/1 这条线内部,用来补齐真正决定端到端时延的协议语义,同时保持现有长连接、固定头、低层二进制热路径原则不变,并把协议定位从“神经渲染专用链路”提升为“面向神经网络运行时场景的轻量实时 AI 领域级应用层协议”。
preview2 关注的问题不再只是“能不能连通”,而是以下四件事:
- 让客户端与服务端可以复用低频对象,避免热路径每次都全量重发稳定内容。
- 让协议显式表达 typed payload / extension frame,使 tensor、token、音视频块、结构化事件和工具增量都能走统一的实时会话语义。
- 让协议显式表达 partial / stale / degrade / supersede 这些低时延场景必需的运行时语义。
- 让收发两端与宿主接入层不必各自私下发明流控、预算、降级、对象引用和传输切换规则。
这里的“轻量实时”同样不意味着 NNRP 要成为通用实时媒体协议。它主要解决的是神经网络场景里“语义对象、推理预算、结果降级、对象引用、传输切换”这些运行时问题,而不是浏览器媒体栈或视频分发栈本身的问题。
本文冻结的是 NNRP/1-preview2 作为开发阶段设计文档时的设计方向、一阶消息语义与实现边界;但本文冻结的代码发包身份是 NNRP/1.0。
1.1 总览图
这张图对应 preview2 的阅读主线:它不只是补字段,而是把对象引用、异步结果泵、显式流控和传输探测同时拉进协议层。
2. preview2 明确负责的主题
NNRP/1-preview2 明确负责以下主题:
- 会话内低频对象的安装、引用、失效与回收语义。
FRAME_SUBMIT的 inline / reference / mixed 三种提交模式。RESULT_PUSH的完整结果、局部结果、陈旧结果和降级结果语义。- 端到端预算协商、服务端降级回执与结果时延口径。
- 显式流控与可观测性,用于限制 in-flight、表达服务端拥塞与指导客户端背压。
- 传输层可插拔设计:定义 TCP+TLS 传输绑定规范,并引入基于吞吐量探测的传输层自动选择机制(Transport Probing Phase)。
- 类型化载荷与扩展帧设计:在不改公共头的前提下,把 tensor、token、音视频块、结构化事件与工具增量纳入统一的数据面模型。
preview2 不负责以下主题:
- 多租户与多连接聚合调度。
- 连接迁移、断点恢复与 resume token 正式化。
- GPU 侧零拷贝、特定渲染管线或 runtime 内部线程模型。
- 最终正式版的多路 QoS 分类与优先级仲裁。
- 面向传统 Web 音视频通话的 ICE/NAT 穿透、设备采集、A/V sync、AEC/NS/AGC、SFU/MCU 等浏览器媒体能力。
- 面向泛视频流分发或视频流云游戏的连续媒体传输栈,例如硬件编解码、jitter buffer、ABR、frame pacing 与显示链路优化。
因此,传统 Web 音视频通话、泛视频直播/点播分发以及视频流云游戏都不应被当作 NNRP/1-preview2 的主目标业务。即便这些场景也强调实时性,它们的主矛盾依然是媒体处理与分发,而不是神经网络运行时语义;NNRP 更适合作为 AI 语义层协议,而不是去直接替代成熟的媒体协议栈。
3. 设计原则
preview2 保持以下原则不变:
- 热路径继续禁止 JSON 与 Protobuf。
- 公共头继续保持固定长度、小端序、显式长度字段与可直接定位的二进制布局。
- 低频对象协商继续走可靠 control stream;高频 typed payload 继续走 submit/result stream。
- 任何对延迟有利的语义都必须先成为协议概念,再落到具体实现中,而不是只做局部私有技巧。
- tensor 仍是首个标准 payload profile,但 preview2 不再把数据面限定为“仅张量”。
- preview2 的规范会话形态是“单 session 长连接上的异步 submit pump + result pump + control side-channel”,而不是“每次
FRAME_SUBMIT都隐含一次同步 request-response 事务”。 FRAME_SUBMIT负责表达提交、预算、依赖与 payload 语义,不应在协议语义上被解释为“提交这一帧并等待匹配结果后才允许继续发送后续帧”。- 若宿主侧仍提供
submit_and_wait一类便捷调用,它只能被视为 smoke / demo 级便捷封装,不得反向定义 preview2 的标准调用模型。
4. 当前 NNRP/1 代码层身份与既有消息延续
4.1 代码层版本身份
preview2 在代码层冻结的发包身份是 NNRP/1.0:
version_major = 1wire_format = 0- ALPN
nnrp/1
这不意味着设计阶段名不再叫 preview2,而是指 preview2 这份开发阶段文档要求代码发出的 wire identity 直接固定为 NNRP/1.0,其中 wire_format = 0,而不是继续保留 2 这种 preview 期内部值,或保留 preview 专用 ALPN。
4.2 公共头
preview2 继续沿用 40 字节公共头,不改变 header 基本形状:
- 继续保留
magic / version_major / wire_format / msg_type / header_len / flags / meta_len / body_len / session_id / frame_id / view_id / route_id / trace_id。 header_len继续固定为40。- preview2 的主要演进点放在消息类型、metadata 字段表、body block 组织规则和 flags 语义扩展,而不是更换公共头。
4.3 既有消息延续
以下既有消息继续保留:
CLIENT_HELLOSERVER_HELLO_ACKSESSION_PATCHSESSION_PATCH_ACKFRAME_SUBMITFRAME_CANCELRESULT_PUSHRESULT_DROPCACHE_PUTCACHE_ACKCACHE_INVALIDATEPINGCLOSEERROR
preview2 优先扩展既有消息的字段与语义,只有在 preview1 消息无法承载新增语义时才增加新消息类型。
5. preview2 必须新增的协议能力
5.1 低频对象引用
preview2 引入“对象级引用是热路径一等公民”的约束。
低频对象至少覆盖以下类别:
- 相机块模板、稳定 camera block 或其他提交上下文模板。
- tile index block、payload layout 模板或其他可复用索引对象。
- tensor section descriptor 表,或 token/audio/video/event 的 schema descriptor。
- 低频 codec table / length table、tokenizer 片段、媒体分片模板或其他编码辅助对象。
- 结果侧可复用的 layout / residual object、prompt segment、tool schema 或会话内稳定对象。
preview2 约束如下:
- 低频对象仍通过
CACHE_PUT / CACHE_ACK / CACHE_INVALIDATE生命周期管理。 FRAME_SUBMIT和RESULT_PUSH必须允许同时携带 inline block 与 cache reference block。- 同一帧内允许“部分对象引用,部分对象内联”,不得强制 all-or-nothing。
- cache object 必须有稳定对象类型、命名空间和版本约束;cache miss 必须显式回报,不得静默回退。
5.2 mixed submit 模式
preview2 将 FRAME_SUBMIT 的提交模式冻结为三种:
inline:全量内联对象块与 typed payload frame。reference:主体仅发送引用句柄与少量动态字段,不重复发送稳定对象。mixed:部分 block 内联,部分 block 用 cache reference。
协议层必须显式告诉服务端:
- 哪些 block 是新内联对象。
- 哪些 block 是对已有 cache object 的引用。
- 哪些 block 被本帧 supersede 或替换。
5.3 partial / stale / degrade 结果语义
preview2 不再把结果简单分成 “有结果” 和 “丢结果”。
RESULT_PUSH 必须显式表达以下结果类别:
complete:完整结果。partial:仅返回部分 tile、部分 section、部分 token/audio/video chunk 或低质量结果。stale_reuse:结果复用了旧帧或旧对象,但仍可展示。degraded:服务端因预算、拥塞或资源限制主动降级。
对应约束:
RESULT_PUSH必须能指明本次结果覆盖了哪些 tile。RESULT_PUSH必须能指明结果是否引用了旧对象、旧 frame 或旧 cache object。- 客户端必须能区分“可展示的降级结果”和“不可展示的结果丢弃”。
5.4 预算与降级协商
preview2 将“预算不是 hint,而是服务端必须可回执的运行时契约”作为明确约束。
协议层至少应覆盖:
- 客户端提交的 frame latency budget。
- 服务端实际采用的执行策略,例如
full / partial / stale / drop。 - 服务端拒绝或降级的稳定原因码。
- 当前结果实际花费的 queue / compute / server_total 指标。
低时延场景下,客户端不能只知道“慢了”,还必须知道“为什么慢了,以及服务端是否已经主动降级”。
5.5 显式流控
preview2 将“单连接上能同时飞多少帧、服务端何时建议背压”正式化。
最小要求:
- 握手中继续协商
max_concurrent_frames。 - 增加运行期 flow update 机制,用于服务端动态收紧或放宽 credit。
- 客户端必须能区分
queue_full、server_busy、budget_exceeded、superseded等不同拒绝原因。
5.5A 持续异步流语义
preview2 额外冻结以下会话级约束:
- 客户端可以在同一 session 内连续提交多个
FRAME_SUBMIT,前提是未超过当前max_concurrent_frames与运行期 credit。 - 服务端可以乱序完成不同
frame_id的结果,只要结果元数据能明确声明frame_id / dependency_frame_id / reused_frame_id / result_class。 - 客户端必须允许在同一长连接上独立接收
RESULT_PUSH / RESULT_DROP / FLOW_UPDATE / RESULT_HINT,而不是把结果读取强耦合到某一次 submit 调用的返回路径上。 stale / superseded / degraded / drop语义的目标,正是让结果消费可以与提交解耦;旧结果可被显式废弃,但这不应阻塞更新帧继续提交。- 因此,preview2 的默认交互形态应是后台结果泵、显式 in-flight 跟踪和按 deadline 消费,而不是逐帧同步等待。
- 这些约束属于 preview2 现阶段要落地的运行时语义,不属于必须推迟到 preview3 的版本级主题;只有当后续需要新的 wire-visible 优先级类、恢复语义或更细粒度多队列调度时,才应进入更后续版本讨论。
5.6 丢包容忍声明
preview2 引入会话级与帧级丢包容忍声明(Loss Tolerance Declaration),允许客户端显式告诉服务端"在当前链路质量下,我允许哪些内容不重传",从而避免高丢包场景下传输层无限重试打爆 RTT 预算、阻塞服务端推理队列或淹没客户端接收缓冲。
设计目标
- 客户端在握手时声明全局丢包容忍策略,服务端依此调整重传优先级与
RESULT_DROP时机。 - 客户端在提交单帧时可以覆盖全局策略,进一步降低或提升该帧的重传要求。
- 策略只影响服务端对"允许丢弃"的判断阈值,不改变 wire 格式,也不替代 QUIC 或 TCP 的传输层重传控制。
丢包容忍等级枚举
preview2 冻结以下 loss_tolerance 枚举值:
| 值 | 名称 | 语义 |
|---|---|---|
0 | strict | 不允许丢弃,所有结果都必须重传直到送达或超时 |
1 | best_effort | 超出延迟预算的结果可丢,不强制重传(默认值) |
2 | low_latency | 优先时延,只要发现 RTT 超过 latency_budget_ms 的 50% 即可提前丢弃 |
3 | fire_and_forget | 完全不要求重传,server 侧每帧结果发一次即止,无论是否收到 ACK |
strict 模式下服务端仍受 latency_budget_ms 约束,超预算后只能返回 RESULT_DROP,而不是无限等待;fire_and_forget 只适合高帧率、可接受帧间跳变的场景,不得用于 frame_class = keyframe 的关键帧。
会话级声明(握手)
CLIENT_HELLO 扩展字段新增 session_loss_tolerance: u8,含义为本会话全局默认丢包容忍等级。若该扩展缺失,服务端行为等价于 best_effort。
SERVER_HELLO_ACK 扩展字段新增 accepted_loss_tolerance: u8,返回服务端实际接受的等级;若服务端不支持客户端请求的等级,应向保守方向对齐并在此字段声明实际生效值。
帧级覆盖(提交时)
FRAME_SUBMIT v2 metadata 新增 loss_tolerance_policy: u8 字段,语义与枚举相同。若值为 0xFF,表示继承会话级策略,不做帧级覆盖。
与现有机制的关系
frame_class = discardable与CAN_DROP flag仍有效,loss_tolerance是在这两者之上的更细粒度策略。latency_budget_ms优先于loss_tolerance:哪怕是strict模式,服务端超出预算后也必须返回RESULT_DROP,而不是继续等。fire_and_forget场景下,服务端不得把未确认的RESULT_PUSH记入重传队列,但仍必须发出一次RESULT_PUSH。- 丢包容忍等级声明不影响控制消息(
CLIENT_HELLO、SESSION_PATCH、CLOSE等)的可靠性要求,这些消息始终必须可靠送达。
5.7 类型化载荷与扩展帧
preview2 不再假设请求体和结果体只能承载 tensor。协议层必须允许在同一会话语义下传递多种高频载荷,同时保持固定布局、显式长度、可快速跳过未知非关键块的设计原则。
preview2 首轮至少保留以下 payload_kind:
tensortoken_chunkaudio_chunkvideo_chunkstructured_eventtool_deltaopaque_bytes
为避免不同实现在位图定义上各自编号,preview2 冻结 payload_kind_bitmap 为 u32,位定义如下:
| bit | 掩码 | payload_kind |
|---|---|---|
| 0 | 0x00000001 | tensor |
| 1 | 0x00000002 | token_chunk |
| 2 | 0x00000004 | audio_chunk |
| 3 | 0x00000008 | video_chunk |
| 4 | 0x00000010 | structured_event |
| 5 | 0x00000020 | tool_delta |
| 6 | 0x00000040 | opaque_bytes |
payload_kind_bitmap 的高位(bit 7-31)在 preview2 中保留,发送端必须清零,接收端必须拒绝未知置位。首轮 critical_extension_frame_bitmap 同样冻结为 u32,但暂不分配具体 bit;在首批标准 extension frame kind 发布前,该位图必须为 0,不得私自占用保留位。
约束如下:
FRAME_SUBMIT与RESULT_PUSH的 body 必须允许由一个 typed payload descriptor table 和一个或多个 typed payload frame 组成;descriptor 至少声明payload_kind / flags / offset / length / profile_id。- tensor 仍是首个标准 profile;camera block、tile index block、tensor section table 作为 tensor profile 下的标准对象种类继续复用。
- token、音视频块、结构化事件和工具增量不得伪装成 tensor section;必须使用明确的
payload_kind标识。 - 扩展帧设计借鉴 HTTP 与 WebRTC 的“类型化帧 + 可跳过扩展”思路,但不引入文本 header map、SDP 风格协商或笨重媒体栈依赖。
CLIENT_HELLO/SERVER_HELLO_ACK必须协商支持的payload_kindbitmap 与关键扩展帧集合;收到未知且关键的 payload/extension frame 时必须显式返回ERROR,不得静默降级。
6. preview2 消息层演进
6.1 新消息类型
preview2 新增以下消息类型:
| 值 | 名称 | 方向 | 说明 |
|---|---|---|---|
0x17 | FLOW_UPDATE | 双向 | 动态调整作用域内 credit、背压窗口与暂停/恢复状态 |
0x18 | RESULT_HINT | S -> C | 返回服务端当前预算策略、拥塞状态与建议降级模式 |
preview2 不新增新的热路径大载荷消息类型,FRAME_SUBMIT 与 RESULT_PUSH 仍是唯一的数据面主体消息;新增的 payload 种类通过 typed payload frame 进入这两类消息,而不是继续增殖顶层 msg_type。
6.1.1 FLOW_UPDATE fixed metadata
FLOW_UPDATE 首轮固定为 32 字节 fixed metadata,用于在控制侧显式表达 credit、背压与暂停/恢复状态。字段顺序冻结如下:
| 字段 | 类型 | 说明 |
|---|---|---|
scope_kind | u8 | 更新作用域 |
update_reason | u8 | 更新原因 |
backpressure_level | u8 | 当前背压等级 |
reserved0 | u8 | 保留,发送端清零 |
connection_credit | u16 | connection 级可并发 credit |
session_credit | u16 | session 级可并发 credit |
operation_credit | u16 | operation 级可并发 credit |
reserved1 | u16 | 保留,发送端清零 |
operation_id | u64 | 当 scope_kind=operation 时指向目标 operation;否则为 0 |
retry_after_ms | u32 | 建议等待窗口;无则为 0 |
credit_epoch | u32 | 同一作用域上的单调递增 credit 更新代号 |
flow_flags | u32 | flow-control 行为位图 |
scope_kind:u8 首轮冻结为:
| 值 | 名称 | 语义 |
|---|---|---|
0 | connection | 更新整条连接的总 credit 或总背压状态 |
1 | session | 更新某个 session 的 credit 或背压状态 |
2 | operation | 更新某个更细粒度在途工作单元的 credit 或背压状态 |
update_reason:u8 首轮冻结为:
| 值 | 名称 | 语义 |
|---|---|---|
0 | grant | 新授予 credit 或放宽限制 |
1 | reduce | 收紧 credit 窗口 |
2 | pause | 暂停继续发送新的提交 |
3 | resume | 从暂停状态恢复 |
4 | congestion | 因拥塞进入限流或背压状态 |
backpressure_level:u8 首轮冻结为:
| 值 | 名称 | 语义 |
|---|---|---|
0 | none | 无背压 |
1 | soft | 建议发送方主动降速 |
2 | hard | 发送方应停止提交新的 in-flight 工作 |
flow_flags:u32 首轮冻结以下位定义:
| bit | 掩码 | 含义 |
|---|---|---|
| 0 | 0x00000001 | credit_valid:对应 scope 的 credit 字段有效 |
| 1 | 0x00000002 | retry_after_valid:retry_after_ms 有效 |
| 2 | 0x00000004 | background_only:只允许后台或低优先级工作继续推进 |
| 3 | 0x00000008 | drain_in_flight_only:仅允许现有 in-flight 工作排空,不再接受新提交 |
| 4-31 | 保留 | 发送端清零,接收端收到未知置位必须拒绝 |
首轮约束:
scope_kind=connection时,headersession_id必须为0;发送方只读取connection_credit,session_credit / operation_credit / operation_id必须为0。scope_kind=session时,headersession_id必须为目标 session;发送方只读取session_credit,connection_credit / operation_credit / operation_id必须为0。scope_kind=operation时,headersession_id必须为目标 session,operation_id必须非零;发送方只读取operation_credit。- 若
retry_after_ms != 0,则flow_flags.retry_after_valid必须置位。 credit_epoch必须在同一作用域上单调递增;接收方不得接受更旧的 update。
6.1.2 RESULT_HINT fixed metadata
RESULT_HINT 首轮固定为 16 字节 fixed metadata,字段顺序冻结如下:
| 字段 | 类型 | 说明 |
|---|---|---|
applied_budget_policy | u32 | 服务端当前建议采用的预算策略 |
congestion_state | u32 | 当前拥塞状态 |
reason | u32 | 给出该提示的主要原因 |
retry_after_ms | u32 | 建议等待窗口;无则为 0 |
applied_budget_policy:u32 首轮冻结为:
| 值 | 名称 |
|---|---|
0 | none |
1 | full |
2 | partial |
3 | stale_reuse |
4 | drop |
congestion_state:u32 首轮冻结为:
| 值 | 名称 |
|---|---|
0 | none |
1 | steady |
2 | elevated |
3 | saturated |
reason:u32 首轮冻结为:
| 值 | 名称 |
|---|---|
0 | none |
1 | queue_full |
2 | server_busy |
3 | budget_exceeded |
4 | superseded |
首轮约束:
RESULT_HINT不携带 body,body_len必须为0。frame_id可指向当前提示主要关联的帧;若提示作用于整个会话,则frame_id可为0。- 若
retry_after_ms == 0,表示本次提示不要求显式等待窗口。
6.2 FRAME_SUBMIT v2 metadata
preview2 的 FRAME_SUBMIT metadata 从 preview1 的固定布局扩展为 v2 版本,新增字段至少包括:
submit_mode:inline / reference / mixed。object_ref_mask:声明本帧哪些 body block 采用引用。budget_policy:声明客户端允许的降级边界,例如是否接受 partial / stale / drop。dependency_frame_id:若本帧依赖旧帧对象,显式标明依赖来源。loss_tolerance_policy:帧级丢包容忍等级(u8);0xFF表示继承会话级策略。payload_kind_bitmap:声明本帧 body 中包含哪些payload_kind。payload_frame_count:声明本帧携带的 typed payload frame 数量。
在完整 v2 metadata 布局冻结前,preview2 先冻结以下字段编码约束,避免不同实现先把局部字段写成不同位宽:
submit_mode: u8,枚举值固定为0=inline、1=reference、2=mixed。budget_policy: u8,按 bitmask 编码:0x01=allow_partial、0x02=allow_stale_reuse、0x04=allow_degraded、0x08=allow_drop;其余 bit 保留且必须为0。loss_tolerance_policy: u8,继续复用5.6中冻结的loss_tolerance枚举;0xFF表示继承会话级策略。payload_kind_bitmap: u32,继续复用5.7中冻结的 bit 定义。payload_frame_count: u16。object_ref_mask: u32,首轮只为 submit 侧标准低频 object slot 分配 bit:bit0=camera_block、bit1=tile_index_block、bit2=tensor_section_table、bit3=payload_layout_template;bit4-31保留且必须为0。inline模式要求object_ref_mask == 0;reference模式要求object_ref_mask != 0且被引用的标准 slot 不得再以内联对象块重复发送;mixed模式要求object_ref_mask != 0且至少保留一个标准 slot 以内联对象块发送。object_ref_mask只是 submit 侧标准 slot 的摘要,不是 body 解码的替代品;真正的引用对象集合仍以 body 中的 object reference block 为准。
6.3 RESULT_PUSH v2 metadata
preview2 的 RESULT_PUSH metadata 从 preview1 的固定布局扩展为 v2 版本,新增字段至少包括:
result_class:complete / partial / stale_reuse / degraded。applied_budget_policy:服务端本帧实际采用的处理策略。reused_frame_id:若结果复用旧帧,明确标明来源。covered_tile_count:本次结果真正覆盖的 tile 数。dropped_tile_count:被主动丢弃或未计算的 tile 数。payload_kind_bitmap:声明本次结果实际返回了哪些payload_kind。payload_frame_count:声明本次结果携带的 typed payload frame 数量。
在完整 v2 metadata 布局冻结前,preview2 同步冻结以下结果侧字段编码约束:
result_class: u8,枚举值固定为0=complete、1=partial、2=stale_reuse、3=degraded。applied_budget_policy: u8,继续复用6.2中冻结的budget_policybitmask;服务端返回值必须是客户端声明策略的子集,或显式通过RESULT_DROP / ERROR失败。covered_tile_count: u16。dropped_tile_count: u16。payload_kind_bitmap: u32,继续复用5.7中冻结的 bit 定义。payload_frame_count: u16。
其中 covered_tile_count / dropped_tile_count 在 tensor profile 下继续有效;对于 token、音视频块、结构化事件等非 tensor payload,覆盖范围与顺序信息通过 typed payload descriptor 与对应 profile-specific extension frame 表达。
6.4 CACHE_* v2 约束
preview2 不改变 CACHE_PUT / CACHE_ACK / CACHE_INVALIDATE 的角色,但强化以下要求:
- cache object 必须带稳定
object_kind。 CACHE_ACK必须指明对象是否可立即进入热路径引用。CACHE_INVALIDATE必须支持按namespace / object_kind / object_key / whole_session四种粒度失效。
为避免 cache 语义在不同实现中漂移,preview2 先冻结以下基础枚举与位宽:
object_kind: u16,首轮标准值为:0x0001=camera_block、0x0002=tile_index_block、0x0003=tensor_section_table、0x0004=codec_table、0x0005=reusable_result_object、0x0006=payload_layout_template、0x0007=prompt_segment、0x0008=tool_schema、0x0009=structured_event_schema。invalidate_scope: u8,固定为:0=whole_session、1=namespace、2=object_kind、3=object_key。- 未分配的
object_kind与invalidate_scope值均保留,发送端必须拒绝私有占位;若后续需要扩展,应由协议文档追加编号而不是局部私有约定。
7. body block 组织规则
preview2 首轮冻结的数据面 body 统一采用“固定 prelude + 固定顺序 region”的模型。FRAME_SUBMIT 与 RESULT_PUSH 的 body 均按以下 region 顺序组织:
BodyRegionPrelude- inline low-frequency object region
- low-frequency object reference region
- typed payload descriptor table
- inline typed payload frame region
- extension frame descriptor table
- extension frame payload region
其中 tensor-centric session 中的 camera block、tile index block、tensor section table 只是标准 object kind / payload profile 的具体实例;token、音视频块、结构化事件和工具增量通过 payload_kind 与 profile-specific payload 解释表达。preview2 首轮不再保留一个单独的“typed payload reference block region”;payload 数据本身的引用式传输不属于本次冻结范围,后续若需要引入,必须在协议文档中追加新的固定布局,而不是在局部实现中私设。
7.1 BodyRegionPrelude 固定布局
每个 preview2 数据面 body 必须以 32 字节 BodyRegionPrelude 开头,字段顺序冻结如下:
inline_object_bytes: u32object_reference_bytes: u32typed_payload_descriptor_bytes: u32typed_payload_frame_bytes: u32extension_descriptor_bytes: u32extension_payload_bytes: u32body_flags: u32reserved: u32
约束如下:
- 各 region 在 body 中必须严格连续拼接,长度分别由上述字段给出;实现不得通过“猜测有没有某类 block”来决定偏移。
body_flags在 preview2 首轮必须为0;reserved必须为0。- 若
payload_frame_count == 0,则typed_payload_descriptor_bytes与typed_payload_frame_bytes都必须为0。 typed_payload_descriptor_bytes必须等于payload_frame_count * 16。extension_descriptor_bytes必须是16的整数倍。
7.2 inline object block 与 object reference block
preview2 首轮冻结两种低频 object block header:
InlineObjectBlockHeader,固定 16 字节:object_kind:u16 + object_flags:u16 + profile_id:u16 + reserved0:u16 + object_bytes:u32 + reserved1:u32。ObjectReferenceBlock,固定 16 字节:object_kind:u16 + ref_flags:u16 + cache_namespace:u32 + cache_key_hi:u32 + cache_key_lo:u32。
约束如下:
InlineObjectBlockHeader.object_flags、reserved0、reserved1在 preview2 首轮必须为0。ObjectReferenceBlock.ref_flags在 preview2 首轮必须为0。- inline object payload 紧跟在
InlineObjectBlockHeader之后,payload 末尾按 8 字节对齐补零。 - submit 侧标准低频 object slot 的规范顺序固定为:
camera_block、tile_index_block、tensor_section_table、payload_layout_template。 - 若 submit 侧某个标准 slot 在
object_ref_mask中置位,则该 slot 必须在 object reference region 中按上述顺序出现一个且仅一个ObjectReferenceBlock,且不得在 inline object region 中再出现同 slot 的 inline object。 - 若 submit 侧某个标准 slot 未置位且本帧需要发送该对象,则必须在 inline object region 中按上述顺序出现对应
InlineObjectBlockHeader + payload。 - result 侧不复用
object_ref_mask;result body 中出现的 low-frequency object block 或 object reference block 由各自 header 自描述,并按(object_kind, cache_namespace, cache_key_hi, cache_key_lo)严格递增排序。
7.3 TypedPayloadDescriptor 固定布局
preview2 首轮冻结 TypedPayloadDescriptor 为 16 字节:
payload_kind: u8descriptor_flags: u8profile_id: u16payload_offset: u32payload_length: u32reserved: u32
约束如下:
payload_frame_count统计的是 logical typed payload frame 数,也就是TypedPayloadDescriptor条目数;它不统计 tensor section 数,也不统计 extension frame 数。- typed payload descriptor table 位于低频 object region 与 object reference region 之后、inline typed payload frame region 之前。
descriptor_flags在 preview2 首轮必须为0;reserved必须为0。- descriptor 的
payload_kind必须属于 metadata 中声明的payload_kind_bitmap;metadata 中未声明的 payload kind 不得只在 body 中偷偷出现。 - descriptor 的
profile_id表示该 payload frame 采用的 profile-specific 解释;0表示不额外绑定 profile-specific 语义,后续若为某类 payload 定义标准 profile,再由协议文档追加编号。 payload_offset / payload_length的语义冻结为“相对于 inline typed payload frame region 起点的字节偏移与字节长度”;同一张 descriptor table 内的 frame 区间必须按 offset 严格递增、不得重叠。- tensor payload 继续走 tensor profile 语义;token、audio、video、structured event、tool delta 与 opaque bytes 不得再伪装成 tensor section、tile coverage 或 tensor 专用 body block。
7.4 ExtensionFrameDescriptor 固定布局
preview2 首轮冻结 ExtensionFrameDescriptor 为 16 字节:
extension_kind: u16extension_flags: u16profile_id: u16reserved0: u16payload_offset: u32payload_length: u32
约束如下:
extension_flags.bit0冻结为critical;其余 bit 保留且必须为0。reserved0必须为0。payload_offset / payload_length相对于 extension frame payload region 起点解释;descriptor 条目必须按 offset 严格递增、不得重叠。- 若
critical == 0且调用方不认识该extension_kind,接收端必须能够在不解码 payload 的前提下快速跳过。 - 若
critical == 1,则该extension_kind必须已经在握手期通过critical_extension_frame_bitmap协商;否则必须显式失败。 - 在 preview2 首轮标准 extension frame kind 尚未分配前,
critical_extension_frame_bitmap仍必须为0,因此所有已落地的 extension frame 都必须使用critical == 0。
7.5 首轮实现边界
preview2 首轮冻结的是上述 body prelude、low-frequency object block/reference block、typed payload descriptor、extension frame descriptor 的固定字节布局,以及各 region 的顺序与 offset 语义。以下内容不属于本次冻结范围:
- typed payload 数据本身的独立 reference block 形态;后续若要引入,必须新增明确 region 或 descriptor 规则。
TypedPayloadDescriptor.descriptor_flags的语义扩展;preview2 首轮必须为0。- 标准 extension frame kind 的具体编号;在编号未分配前不得私设关键扩展。
在该边界内,实现侧可以继续落地 inline typed payload、low-frequency object reference、unknown non-critical extension fast-skip,以及显式 body ordering;超出该边界的 payload-data reference 设计不得再以局部私有方式推进。
8. preview2 成功标准
preview2 的成功标准不是“字段更多”,而是以下结果成立:
- 收发两端可以在不改公共头的前提下实现 mixed submit / partial result。
- 低频稳定对象在热路径上可以引用而不是重发。
partial / stale / degraded / drop能被协议显式区分并落到客户端行为中。- 运行期 flow control 不再完全依赖局部私有实现,而是成为 wire-visible 语义。
- 同一套协议可以承载 tensor、token、音视频块、结构化事件和工具增量,而不需要为每类 AI workload 重新发明一条传输链路。
- 协议默认交互形态应能支持多帧 in-flight 与独立结果泵,而不是把
FRAME_SUBMIT -> RESULT_PUSH固化成逐帧同步 API。
9. 协议边界
preview2 冻结的是协议对象、消息语义、metadata 字段表、body 布局、transport probing 与结果口径。
具体实现可以在这一边界内提供异步提交泵、结果泵、重放工具与宿主接入封装,但不得改变已冻结的字节布局、状态机和错误口径。
如果只是把 preview2 已有的 submit/result/control 语义落成真正的异步持续流调用模型,这项工作仍属于 preview2 范围;不应因为现有便捷封装仍保留逐帧 submit_and_wait 就把该语义推迟到 preview3。
10. 传输层可插拔设计与 Transport Probing
10.1 设计原则
preview1 的 wire codec(公共头、metadata、body block)已内建传输中立性:header 自描述长度,可在任意可靠字节流上完整解析。preview2 将这一设计意图正式化为单协议多传输绑定规范。
preview2 在 endpoint 语义上只保留一个安全 URI scheme:nnrps://。scheme 只表达“这是一个走 TLS 保护的 NNRP endpoint”,不表达具体底层 transport binding;是否走 QUIC、TCP 以及未来其他路径,应由客户端选路策略与服务端能力共同决定,而不是继续为每种路径发明一个新的 scheme。
preview2 首轮冻结两种传输绑定:
- QUIC binding:ALPN
nnrp/1。 - TCP binding:TLS 单连接长连接,ALPN
nnrp/1-tcp。
两种 binding 在协议层完全等价,客户端应通过 Transport Probing Phase 选择吞吐量与响应时间更优的路径,而不是预设 QUIC 为首选。这与 preview1 的"固定 QUIC"形成对比,也是相比 WebRTC 的核心差异:我们在握手前进行 RTT、抖动、吞吐量、限流检测,基于实测性能而非配置硬编码来选择传输层。握手(CLIENT_HELLO / SERVER_HELLO_ACK)可以在探测选定的任一传输路径上进行。
若客户端已经知道要强制某条路径,则应在首个包发送前通过本地 dial policy 直接选定该 binding,而不是依赖新的 URI scheme。为让这一意图在协议层可见,preview2 要求 CLIENT_HELLO 扩展字段携带 transport_policy(例如 auto / prefer_quic / prefer_tcp / force_quic / force_tcp)与可选的 preferred_transport_id;SERVER_HELLO_ACK 返回 active_transport_id,必要时回显被接受或降级后的策略。这样既保留自动选路,又允许显式指定 transport,而且不会把 transport 枚举硬编码到 endpoint scheme 里。
为避免实现侧各自发明 control_extension_block 的握手扩展编号,preview2 在 CLIENT_HELLO / SERVER_HELLO_ACK 中冻结以下扩展类型:
ext_type | 承载消息 | 名称 | payload 说明 |
|---|---|---|---|
0x0101 | CLIENT_HELLO | transport_policy | transport_policy:u8 + reserved:u8 + reserved:u16 + preferred_transport_id:u32 |
0x0102 | SERVER_HELLO_ACK | transport_policy_ack | transport_policy:u8 + accepted_transport_policy:u8 + reserved:u16 + active_transport_id:u32 |
0x0103 | CLIENT_HELLO | loss_tolerance | session_loss_tolerance:u8 + reserved:u8 + reserved:u16 + reserved:u32 |
0x0104 | SERVER_HELLO_ACK | loss_tolerance_ack | accepted_loss_tolerance:u8 + reserved:u8 + reserved:u16 + reserved:u32 |
0x0105 | CLIENT_HELLO | payload_capabilities | payload_kind_bitmap:u32 + critical_extension_frame_bitmap:u32 |
0x0106 | SERVER_HELLO_ACK | payload_capabilities_ack | accepted_payload_kind_bitmap:u32 + accepted_critical_extension_frame_bitmap:u32 |
其中 0x0103 / 0x0104 只冻结 ext_type 与最小 payload 形状,不额外引入新语义字段;后续若需补更多协商信息,应新增新的 ext_type,而不是重解释这些保留位。
preferred_transport_id / active_transport_id / old_transport_id / new_transport_id 统一复用同一套 transport_id: u32 编号:0=unspecified、1=quic、2=tcp。除 preferred_transport_id 可用 0 表示“不额外指定绑定”外,其余实际生效的 transport id 不得为 0。
两种 binding 共用同一份 wire codec,不引入新的公共头字段;transport 策略放在 CLIENT_HELLO / SERVER_HELLO_ACK 的扩展字段中,而不是放进公共头或为每条路径单独创建 scheme。
10.2 TCP binding 语义约束
TCP binding 与 QUIC binding 的主要差异:
- 所有消息共用单一 TLS 字节流,按
header.msg_type + header.frame_id进行应用层路由。 - 无 QUIC stream 级并发隔离,Head-of-Line blocking 重新出现;这是已知取舍,不是缺陷。
- Datagram 语义(
FRAME_CANCEL、PING等)在 TCP binding 下仍通过可靠流发送,不另定义丢包语义。 - 两种 binding 的 session 状态机、帧生命周期、错误映射完全一致。
10.3 Transport Probing Phase
preview2 引入 Transport Probing 作为 CLIENT_HELLO 之前的可选前置阶段。
探测目标
客户端不能仅靠 ICMP ping 平均 RTT 判断传输选择,因为 ISP 对 UDP/QUIC 的限速策略作用于 bulk flow,小包不受影响。探测包必须携带接近真实 payload 体量的数据量才能返回有效吞吐量指标。
探测消息类型
preview2 新增两个消息类型:
| 值 | 名称 | 方向 | 说明 |
|---|---|---|---|
0x19 | TRANSPORT_PROBE | C → S | 客户端发起传输层探测,body 为填充字节,大小接近真实提交载荷 |
0x1a | TRANSPORT_PROBE_ACK | S → C | 服务端回执探测,含接收时间戳 |
TRANSPORT_PROBE 的 metadata 冻结为 16 字节:
| 字段 | 类型 | 说明 |
|---|---|---|
probe_id | u32 | 客户端生成的单起探测标识符 |
probe_payload_bytes | u32 | 本次探测 body 实际字节数 |
client_send_ts_us | u64 | 客户端发送时刻(微秒) |
TRANSPORT_PROBE_ACK 的 metadata 冻结为 16 字节:
| 字段 | 类型 | 说明 |
|---|---|---|
probe_id | u32 | 回射客户端的 probe_id |
reserved | u32 | 保留 |
server_recv_ts_us | u64 | 服务端收到探测包的时刻(微秒) |
探测流程
1. 客户端并发向服务端发送多样本探测:
- QUIC 探测:至少 3 个 scored `TRANSPORT_PROBE`(body ≈ 32KB,走 QUIC binding)
- TCP 探测:至少 3 个 scored `TRANSPORT_PROBE`(body ≈ 32KB,走 TCP binding)
- 实现可额外发送 1 个 warm-up probe,但 warm-up sample 不参与最终评分
2. 服务端两路均监听,收到 `TRANSPORT_PROBE` 就回 `TRANSPORT_PROBE_ACK`
3. 客户端对每个成功样本计算:
- `rtt_us = ack_recv_at - client_send_ts`
- `throughput = probe_payload_bytes / rtt_us`
4. 客户端按 binding 聚合探测结果,默认排序规则冻结为:
- 先比较 `success_count`(成功样本数更多者优先)
- 再比较 `median_throughput`(中位有效吞吐量更高者优先)
- 如仍并列,再比较 `median_rtt_us`(中位 RTT 更低者优先)
5. 若仅一路存在成功样本,直接选那路;若两路都有成功样本,按上述排序规则选择胜者,并在该路上发起正式 `CLIENT_HELLO`
6. 如果两路均无成功样本,返回连接失败错误以上规则冻结的是 preview2 默认客户端选路策略,而不是新的 wire 字段;所有客户端实现都应默认保持同一排序口径,避免在同一网络条件下做出不同 transport 决策。
可选性与向后兼容
- Transport Probing Phase 是可选的。如果客户端已知道平台和网络情况,或本地 dial policy 已强制指定 binding,可以跳过探测阶段直接发起
CLIENT_HELLO。 - preview1 不定义
TRANSPORT_PROBE,preview1 客户端不得发送此消息。 - preview2 服务端必须支持两种 binding 的探测监听,但可以拒绝探测包并返回
ERROR。
10.4 会话中途断路的不断连 fallback
preview2 需要覆盖“已建立会话后,当前传输路径在运行中退化或中断”的恢复路径,目标是减少用户可见中断,而不是强制重建整个业务状态。
设计目标
- 当 QUIC 路径被运营商限速或中断时,客户端可在同一业务会话语义下切换到 TCP binding。
- 切换期间尽量保留最近可展示结果,并避免已确认帧重复提交。
- 切换行为对
FRAME_SUBMIT / RESULT_PUSH / RESULT_DROP的语义保持一致,不引入新数据面格式。
新增控制消息
| 值 | 名称 | 方向 | 说明 |
|---|---|---|---|
0x1b | SESSION_MIGRATE | C → S | 客户端声明从旧传输路径迁移到新路径,请求继续同一 session_id |
0x1c | SESSION_MIGRATE_ACK | S → C | 服务端确认迁移窗口与续传游标 |
SESSION_MIGRATE metadata 冻结为 24 字节:
| 字段 | 类型 | 说明 |
|---|---|---|
old_transport_id | u32 | 旧传输路径标识(例如 QUIC) |
new_transport_id | u32 | 新传输路径标识(例如 TCP) |
last_result_frame_id | u64 | 客户端最后成功接收结果的 frame_id |
client_migrate_ts_us | u64 | 客户端发起迁移时间戳 |
SESSION_MIGRATE_ACK metadata 冻结为 24 字节:
| 字段 | 类型 | 说明 |
|---|---|---|
accept_code | u32 | 0=accepted, 非 0 表示拒绝原因 |
resume_from_frame_id | u64 | 服务端确认的恢复起点 |
grace_window_ms | u32 | 旧路径保活宽限时间 |
server_migrate_ts_us | u64 | 服务端确认时间戳 |
迁移流程
1. 客户端在活跃路径上持续做轻量健康检查(RTT、超时率、有效吞吐)。
2. 当连续 N 个窗口触发退化阈值(例如有效吞吐低于阈值,或 ACK 超时率超限),客户端并发启动备选路径:
- 先执行 Transport Probing(若近期无有效探测结果)
- 在候选路径上建立新连接并发送 SESSION_MIGRATE
3. 服务端校验 session_id 与迁移令牌后返回 SESSION_MIGRATE_ACK,并给出 resume_from_frame_id。
4. 客户端在新路径继续发送 FRAME_SUBMIT;对于 frame_id < resume_from_frame_id 的帧不得重放。
5. 旧路径进入宽限期:
- 仅允许接收已在途 RESULT_PUSH/RESULT_DROP
- 宽限期结束或新路径稳定后关闭旧路径
6. 若迁移被拒绝,客户端可回退到“新建 session + 全量握手”作为最终兜底。行为约束
- 同一时刻最多一个主用传输路径,避免双写导致顺序歧义。
frame_id单调递增规则在迁移前后保持不变。- 迁移期间允许
RESULT_DROP(reason=superseded|expired)增加,但不得把迁移失败静默映射为普通丢帧。 - 客户端与服务端必须记录迁移事件埋点:迁移触发原因、切换耗时、恢复帧游标、是否成功。