Unified Bus 背后的思考
Unified Bus 的协议文档终于发布了。协议最初的设计大多数是四五年前的工作了,我也有两年多没有继续做网络互联方面的工作,但今天读到这本 500 多页的文档,还是倍感亲切。
与大多数协议文档一样,UB 文档介绍了 Unified Bus 协议的大量细节,但很少涉及它设计背后的思考。作为曾在早期参与 UB 项目的一名小兵,介绍一些我个人的思考。今天产品化的 UB 可能与我们当年的设计有诸多不同,因此不要把本文作为权威指南。当成段子看就行了。
为什么要做 UB
要理解 Unified Bus (UB) 诞生的必然性,我们必须回到一个计算机体系结构中的根本性矛盾:总线(Bus)与网络(Network)的割裂。
长久以来,计算机世界被这两种截然不同的互联范式划分为一个个孤岛。
- 在孤岛内部(例如一台服务器或一个机箱内),我们使用总线技术,如 PCIe 或 NVLink。它们是为紧耦合系统设计的,设备间共享着统一的物理地址空间,通信延迟可以做到纳秒级,带宽极高。这是性能的天堂,但这个天堂的疆域极其有限——总线的物理距离和可连接的设备数量都受到严格限制。
- 在孤岛之间,我们则依赖网络技术,如以太网或 InfiniBand。它们为松耦合系统而生,擅长将成千上万的节点连接起来,具备超强的扩展性。但这种扩展性是有代价的:复杂的协议栈、额外的转发开销、微秒甚至毫秒级的延迟,都让网络的性能与总线相比,存在着数量级的鸿沟。
这种”内外有别”的架构,在很长一段时间里是行之有效的。然而,一个幽灵开始在计算机世界上空盘旋——Scaling Law。
大约 10 年前,深度学习领域的研究者们发现了一个惊人的规律:只要持续增大模型规模、数据量和计算量,模型的性能就会随之可预见地、持续地提升。这个发现彻底改变了游戏规则。曾经被认为是”足够用”的单机 8 卡配置,在动辄百亿、千亿参数的巨型模型面前,瞬间变得杯水车薪。
此时,一个清晰而迫切的需求摆在了所有系统架构师面前:我们能否推倒总线与网络之间的这堵墙?我们能否创造一种统一的互联,既拥有总线级的编程简易度和极致性能,又具备网络级的超大规模扩展能力?
这正是 UB 的核心使命。它不仅仅是对现有协议的修补或改良,而是一次彻底的重构。UB 的目标,是构建一个真正的”数据中心计算机”(Datacenter-scale Computer),将整个集群的异构算力、内存、存储无缝地连接成一个统一的、可编程的整体。在这个愿景中,访问一台远程服务器上的内存,应该像访问本地内存一样简单自然;上万个处理器协同计算,应该像在一块芯片上一样高效。
主从架构与对等架构
传统的计算机系统中,CPU 和其他设备(如内存、存储、网卡)之间通常是主从架构。CPU 是主(Master),负责发起和控制所有的数据传输,而其他设备是从(Slave),被动地响应 CPU 的指令。PCIe、RDMA 都是这种主从架构的产物。在 CPU 性能服从摩尔定律一骑绝尘的几十年前,主从架构有其历史优势。但在异构计算成为主流的今天,主从架构就日益成为现代计算系统的瓶颈。
- 性能瓶颈:所有 I/O 操作都需要 CPU 介入,随着设备数量和速度的增加,CPU 成为整个系统的瓶颈。
- 延迟较高:数据路径长,需要经过多层软件栈,带来额外的软件开销和数据拷贝,导致延迟增加。即使 RDMA 等技术可以实现 CPU 上的用户态软件直通网卡,仍然受限于 PCIe uncacheable 的诸多限制,无法实现真正的分布式共享内存。
- 扩展性差:在异构计算场景下,大量 GPU、NPU 等智能设备都需要和 CPU 通信,主从架构难以高效扩展,无法形成设备间的高效”横向”数据交换。
为了打破这一瓶颈,UB 提出了一种对等架构。在 UB 的世界里,所有设备都是平等的,可以被看作是一个个内存块。任何设备都可以通过 Load/Store 这样的内存语义,像访问本地内存一样,直接访问其他设备的内存,而不需要对方 CPU 的干预。这使得数据路径可以完全绕过操作系统,实现零拷贝和微秒级的超低延迟。
这种对等架构带来了许多好处。例如,不同服务器的内存可以组成一个共享的内存池,一个计算密集的应用服务器上空闲的内存,可以被一个内存密集的应用服务器高效利用。各种异构的计算资源、存储资源也可以池化,根据应用的需求动态组合,提高了资源利用率,也减少了不必要的数据搬运。
总线与网络
要理解 UB 的设计哲学,就需要理解总线和网络的根本区别。当然,我们不应陷入抠概念的辩经,现代的总线(如 PCIe)也借鉴了网络的交换思想,但从设计目标和应用规模上看,它们的范式差异是显著的。
特性 | 总线 (Bus) | 网络 (Network) |
---|---|---|
设计范式 | 为节点内通信设计,是紧耦合系统。设备共享物理线路,通过仲裁机制决定使用权。 | 为节点间通信设计,是松耦合系统。数据被切分为报文,通过交换机存储转发。 |
地址空间 | 通常是统一物理地址空间。CPU 通过内存映射IO (MMIO) 访问设备。 | 每个节点有独立地址空间。通过独立的网络地址(如 IP)进行消息传递。 |
拥塞控制 | 通过底层硬件仲裁和信用 (Credit) 机制进行流控,问题相对简单。 | 拥塞是常态,需要复杂的端到端拥塞控制算法(如 TCP、UB C-AQM)来保证稳定和公平。 |
优势 | 延迟极低,带宽极高。 | 扩展性极好,可连接成千上万节点。 |
劣势 | 扩展性差,物理距离和设备数量非常有限。 | 协议栈复杂,转发和处理开销相对高。 |
传统上,我们在一个”超节点”(例如一台服务器或一个机箱)范围内使用总线技术以追求极致性能;而在超节点之间,则使用网络技术以追求大规模扩展。这是两种完全不同的技术栈和编程抽象。
UB 的核心价值在于,它在架构和编程抽象上实现了统一。无论物理上是超节点内的高速电信号背板,还是超节点间的长距离光纤,UB 都为上层应用提供了统一的内存语义。
这意味着,UB 承认在底层的物理实现上,超节点内(更像总线)和超节点间(更像网络)的互联技术可以是不同的,但它通过一层统一的抽象,将这种物理差异向应用屏蔽了。这最终实现了”鱼与熊掌兼得”:既有总线级的编程简易度和高性能潜力,又具备网络级的超大规模扩展能力。
总线与网络的区别,并非对错之分,而是在不同尺度下的范式差异。正如牛顿力学在宏观低速世界中足够精确和简洁,而我们只有在接近光速或深入微观时才需要相对论和量子力学。长久以来,我们在’机箱内’这个宏观世界里安心使用总线这个经典范式,而在’数据中心’这个相对论尺度上则依赖网络。然而,AI 的 Scaling Law 如同一种新的观测工具,它将计算的需求推向了极致,让两个尺度之间的’裂痕’——即通信鸿沟——变得无法忽视。这正是 UB 诞生的历史必然性:我们需要一个能统一这两个尺度的新范式。
太阳底下没有新鲜事
太阳底下没有新鲜事。在一个领域做了一段时间后,就会发现解决问题就像搭积木,首先列出有哪些关键问题,然后对每个关键问题,从已有的方案中选出一个,组合起来就行了。
例如网络,最关键的问题就几个:
- 给应用程序提供什么编程抽象?
- 编程抽象在什么层次上实现,在硬件、操作系统、编程语言和运行时,以及应用程序之间,如何划分?
- 在上述功能划分的基础上,软硬件之间的接口如何设计?
- 每个设备归谁管理?哪些设备一起上电启动?
- 报文在网络上用什么粒度切分传输?
- 采用什么方式分配地址?
- 网络用什么拓扑结构?
- 网络中的节点用什么方式发现?
- 有了地址之后,怎么做路由?要不要支持多路径?
- 点到点,网络每条链路上的的流控(flow control)要不要做,怎么做?
- 端到端,跨越多条链路的的拥塞控制(congestion control)要不要做,怎么做?
- 是否提供可靠传输的语义,如果是,丢包如何检测,如何重传?其他故障如何处理和上报?
- 是否提供保序传输的语义,如果是,如何实现?
- 是提供字节流语义,还是消息语义?
- 是否提供共享内存的语义,如果是,是否提供缓存一致性?共享内存的访问是可以用单条硬件指令实现,还是需要软件执行多条指令?
- 如果编程抽象中提供了其他语义,如何实现?
- 认证鉴权和加密问题怎么解决?
这些问题都想清楚了,回答了,设计就做得七七八八了。类似的这么一种方法在其他领域其实也适用。例如,今天的 AI Agent,无非是选哪个模型,用户记忆怎么实现,知识库怎么实现,上下文工程用哪几种技巧,工具集合有哪些,哪些工作流需要提取成 sub-agent。
单边语义与双边语义
单边语义(内存语义)
《神雕侠侣》中杨过和小龙女的十六年之约,就是单边语义很好的一个例子。杨过与小龙女在绝情谷底身中情花剧毒,小龙女自知时日无多,为求解药,也为激励杨过求生,她选择跳下断肠崖,并在崖壁上刻下”十六年后,在此相会,夫妻情深,勿失信约”。她留下这行字,是希望杨过相信她仍然在世,并以此为信念,耐心等待十六年。刻字之后,她便纵身跳下了悬崖。
小龙女在崖壁上刻字,就是一个单边的”写”操作,她并不需要杨过在场确认。十六年后,杨过如约而至,看到了崖壁上的字,并最终在谷底与小龙女重逢。杨过的”读”操作,同样是单边的,他只是读取崖壁上的信息,而不需要小龙女在场。在计算机网络中,这种通信模式被称为”单边语义”。信息的发送方(小龙女)可以将数据直接写入接收方(杨过)可以访问的某个位置(崖壁),而接收方可以在自己方便的时候去读取,整个过程无需双方同时在线。
由于单边语义主要是读写操作,它又称为内存语义。
注意,单边语义读写的对象不一定是内存地址。凡是依赖共享存储进行通信的都属于单边语义。例如,Redis 等 Key-Value Store 提供的也是一种单边语义,这里的 key 不再是内存地址,而是一个字符串。
从杨过和小龙女的故事中,我们也能发现单边语义的一个缺点:它没有办法通知接收方,发送方也无法得知接收方是否收到了这个信息。杨过如果没有注意看,他就会错过崖壁上的字。杨过有没有看到崖壁上的字,小龙女也无法得知。
双边语义(消息语义)
为了解决这个缺点,需要发送方和接收方配合的双边语义应运而生。计算机网络中最早的语义都是双边语义:从最早的发送网络数据包、接收网络数据包,然后发展到通过连接,发送数据、接收数据。
由于双边语义主要是消息收发操作,它又称为消息语义。
聪明的读者容易发现,写一个内存地址,跟发一个消息给对面的应用,看起来也很类似啊?内存地址是一个数,给对方发个消息是 IP 地址和端口号,看起来没有什么区别啊?
这里的关键区别在于 “写” 操作的语义。写内存地址时,每个地址里只能存一个数据,新的数据写入,旧的数据就被覆盖了。而发消息不一样,尽管也只要一个目的地址,但发的所有消息都会保存在对端。如果一个应用只需要接受来自固定发送方的消息,那么消息语义也很容易用内存语义来实现——只要发送方自己确保数据不会互相覆盖就行了。但如果有多个发送方需要在不确定的时间向同一个接收方发送消息,并且还需要及时通知接收方,单纯的内存语义就很麻烦了——如何协调这些发送方,让它们写内存地址的时候不要发生冲突呢?此时消息语义就更合适了。
消息语义听起来很好,但在高性能网络中,它往往容易导致性能问题。每次接收消息时,接收方的 CPU 都需要处理这个消息。如果只是想读取一块数据,却需要麻烦接收方的 CPU 处理,那么性能肯定没法很高。
更重要的是,消息语义需要接收方预先准备内存缓冲区。那么如果接收方事先不知道要接收的消息有多大,那该准备多少缓存区呢?如果接收方要从多个发送方分别接收消息,还得做好多个发送方几乎同时发送的缓冲区准备。一旦接收缓冲区不足,就会导致发送失败。
从第一性原理出发,双边消息语义更适合用于通知,而单边内存语义更适合用来传输大块数据。就像我要把一个大文件传输给另一个人,多半是把大文件上传到网盘,再发一封邮件通知对方去网盘下载,而不是把大文件直接作为邮件的附件。其中上传到网盘和从网盘下载就是单边语义,而发邮件通知就是双边语义。
UB 协议正是提供了这种单边内存操作,允许一台服务器直接读写另一台服务器的内存,而无需对方 CPU 的干预,从而实现了极高的数据传输效率和极低的延迟。
对于双边语义,认识到双边语义最重要的作用是通知应用,是很重要的。如果一个应用有多个消息等待处理,只要把它们放入一个队列,然后唤醒一次就行了。等应用唤醒后,它自然会依次处理队列中的所有消息。传统上,这些报文处理和进程的事件通知都是通过中断和操作系统完成的。而在 UB 中,硬件可以完成数据面上大多数的任务,从而大大降低操作系统的开销。
当然,学过分布式系统的都知道,内存语义和消息语义是可以互相实现的。但能实现不意味着实现效率高。因此,在不同的场景中,两种语义都有存在的价值。UB 的关键是提供高效的内存语义,使得大块数据的传输、共享数据的访问更加高效。
语义的融合:带立即数的高效通知
我们希望应用使用单边内存语义传数据,使用双边消息语义发通知。但在实践中,这两者常常是耦合的:应用在完成一次大规模数据写入后,几乎总需要再发起一个独立的消息来通知对端”数据已备好”。这个”先写数据、再发通知”的两步操作,引入了额外的网络延迟和软件开销。
为了消除这种低效,UB 协议引入了一项优雅的创新:带立即数(with immediate)的操作。它将数据传输和轻量级通知融合为单个硬件原语,彻底消除了应用层发起第二次通知操作的必要。
1. Write with Immediate
标准的 Write
是一种纯粹的单边操作,数据被”静默”地写入目标内存,接收方的应用对此毫不知情。而我们提出的 Write with Immediate
则在写入数据后,通知对方应用:
- 首先,它像普通的
Write
一样,由硬件完成一次高效的、绕过对端 CPU 的单边内存写入。 - 关键在于,在数据写入完成后,UB 硬件会在接收端的 JFC 中,生成一个完成事件。
- 这个完成事件就像一个双边消息一样,起到了通知对端应用的作用。
- 更进一步,这个由
Write
操作在远端生成的完成事件,同样可以携带一个由发起方提供的 8 字节立即数。
Write with Immediate
就像是网盘在文件上传成功后,自动帮你给接收方发送了一封带有备注(立即数)的通知邮件。它将”传输大块数据”和”发送通知”这两个逻辑步骤,在硬件层面融合成了一个原子操作,极大地简化了应用逻辑,并且避免了单边写入消息和双边通知消息之间乱序的问题。
2. Send with Immediate
这是对标准双边消息的增强。在发起 Send
操作时,除了常规的数据缓冲区,应用还可以附带一个 8 字节的”立即数”。这个立即数不会被写入接收方的内存缓冲区,而是由硬件直接传递到接收方该消息对应的 JFC 完成记录中。这意味着,接收方应用在收到消息完成通知的那一刻,就能立刻从完成记录中读到这 8 字节的元数据,无需再对内存进行额外的读取操作。这对于传递消息类型标签等小块元数据非常高效。
有连接和无连接语义:Jetty 抽象
RDMA “连接”的可扩展性挑战
在 UB 这种颠覆性的范式出现之前,网络领域的工程师们如同在’常规科学’阶段的科学家,致力于在现有的’面向连接’范式内解决难题。RDMA 的出现本身就是一次巨大的成功,但随着数据中心规模的扩张,其固有的可扩展性问题逐渐成为新的’谜题’。在 RDMA 中,通信前必须先”建立连接”,这个”连接”的实体就是队列对(Queue Pair, QP)。每个 QP 都包含了发送队列(SQ)和接收队列(RQ),以及一整套相关的状态机,用于处理包序、重传、确认等复杂的可靠性逻辑。
这种设计的代价是,每一个 QP 的状态都必须完整地保存在网卡的片上内存(SRAM)中,以便硬件能以线速进行处理。在小规模的高性能计算集群中,这不成问题。但当我们将这个模型应用到上万台服务器、每个服务器上又运行着成百上千个应用进程的超大规模数据中心时,这套模型就撞上了”可扩展性的天花板”:
- 硬件资源耗尽:一台服务器要和另外 1000 台服务器通信,就需要维护 1000 个 QP。网卡的片上内存资源极其宝贵,很快就会被耗尽。
- 管理复杂度爆炸:应用程序和操作系统需要管理海量的连接状态,这本身就带来了巨大的软件开销。
为了解开这个谜题,社区付出了巨大的努力,发展出了 XRC(eXtended Reliable Connection)和 SRQ(Shared Receive Queue)等技术。
- SRQ 允许多个 QP 共享同一个接收队列,这在一定程度上减少了接收缓冲区的内存占用,但发送方依然需要为每个对端维护独立的 QP。
- XRC 更进一步,允许多个远端节点共享同一个目标 QP,进一步减少了连接状态。
然而,这些技术本质上都是在原有”面向连接”模型上的”补丁”,它们让模型变得更复杂,但没有从根本上解决问题。当 Scaling Law 这一巨大的’反常’现象出现时,我们意识到,需要的不再是更精巧的补丁,而是一场彻底的范式革命——只要通信还需要应用去显式地创建和管理一个”连接”状态,可扩展性的天花板就永远存在。
从 “连接” 到 “码头”
当时我们意识到,必须彻底抛弃面向连接的思维模型, 釜底抽薪。这个想法最终催生了 UB 的核心抽象:Jetty。
传统的”连接”模型,好比是两个港口之间开辟了一条专属的、点对点的私人航道。而从第一性原理看,通信的本质无非是 “将一份信息,从 A 点可靠地送到 B 点”。通信领域的很多概念,例如 port(港口)、beacon(灯塔)、ping(声纳发出的声音)、gateway(可以通航的水道或海峡)、firewall(船上的防水防火隔板),都起源于航海领域。程传宁老师给我们的无连接抽象起了个 jetty 的名字,它的本意是从岸上伸出到海里的人造物,例如防波堤或码头。
我们特意选择了’Jetty’(码头)这个词,而没有沿用网络领域常见的术语。库恩在《科学革命的结构》中提到,新范式的建立往往伴随着新语言的诞生。旧的词汇,如’连接’,承载了太多旧范式的思维惯性。创造一个新词,是为了强制我们用一种全新的世界观去思考问题——不再是点对点的’私有航道’,而是多对多的’公共码头’。这套新的词汇体系构成了 UB 这个新范式的’行话’,它们是进入这个新世界的’教科书’。
最初,我们曾设想过一种更简单的模型。Jetty 就像一个皮划艇下水点。应用线程(皮划艇爱好者)将请求(皮划艇)放入 JFS 后就可以立刻离开,下水点瞬间就可供下一个人使用。这听起来非常高效,因为它将硬件和软件完全解耦。
然而,这个看似简单的设计却隐藏着一个致命缺陷:它无法实现可靠的软硬件流控。硬件完成任务的速度可能快于软件处理完成事件的速度。如果硬件不断地将完成事件(CQE)投递到 JFC,而软件来不及取走,JFC 很快就会被填满。一旦 JFC 溢出,后续的完成事件就会被丢弃,导致灾难性的后果——软件将永远无法得知某些操作已经完成。
正是为了解决这个问题,最终的设计采用了更为精巧的”泊位”模型。我们可以将 Jetty 想象成一个拥有多个泊位(berth)的公共码头。每个需要出海的请求(一个独立的双边消息或一个内存读写请求),就像一艘船,需要先在码头申请一个泊位。这个泊位,就是 Jetty 中一个被占用的资源格子。在 UB 的具体实现中,当应用将请求(WQE)提交到 JFS(Jetty For Send)后,这个请求就占据了 JFS 中的一个格子。
关键在于,这个格子并非转瞬即逝的。JFS 中的每个格子都与 JFC(Jetty For Completion)中的一个格子一一对应。当硬件完成了网络传输和远端操作后,它会将一个完成事件(Completion)放入 JFC 中对应的格子里。只有当应用程序处理了这个完成事件后,这个格子(即”泊位”)才算被彻底释放,变为空闲状态,可供下一个请求使用。这个 JFS-JFC 配对的机制,也构成了 CPU 与网卡之间精巧的硬件流控:JFC 中未被软件处理的完成事件,会反过来阻止硬件在 JFS 中接收新的请求。
因此,Jetty 中的不同请求,确实更类似船舶码头里泊位的概念。一个请求从提交,到网络传输,再到发起方软件最终处理完完成事件,整个生命周期内都会持续占用码头上的一个泊位。这种设计虽然比”皮划艇下水点”模型复杂,但它通过 JFS 和 JFC 的一一对应关系,建立了一个背压(Back Pressure)机制,从根本上解决了软硬件速度不匹配可能导致的事件丢失问题。
这种模式的根本优势在于,它把一个 N x N 的”私有航道”管理问题,简化成了 N 个”公共码头”的管理问题,解决了可扩展性难题。
Jetty 模型的实践考量:HOL 阻塞、公平性与隔离
当然,”公共码头”模型也必须面对现实世界的复杂性。
首先是 队头阻塞 (Head-of-Line Blocking) 问题。由于 Jetty 中的每个泊位(JFS/JFC 资源对)都需要在请求的整个生命周期内被占用,HOL 阻塞问题是客观存在的。在一个先进先出(FIFO)的队列中,如果排在队首的是一个巨大且耗时的任务(比如一次超大数据的发送),它就会长时间占用一个泊位。如果在这个巨大的发送任务之后,同一个 JFS 中放入了大量小的发送任务,有可能把整个队列(环形缓冲区)的空间占满,此时即使一些小的发送任务已经完成,新的发送任务也无法放入 JFS,因为队列中第一个巨大的发送任务一直没有完成。
不过,这个问题在实践中通常不会造成严重困扰。首先,一个 Jetty 可以拥有的”泊位”数量非常多,可以达到上千的量级。其次,UB 是一个非常快的网络,大多数请求的生命周期极短。因此,在大多数场景下,HOL 导致所有泊位被填满的概率并不高。
其次是 公平性 (Fairness) 与隔离 (Isolation) 问题。既然所有出港的船都从同一个码头出发(单个 JFS),那么就无法保证不同目的地、不同优先级船只的公平性。一个”疯狂”的货主(某个应用)可能会持续不断地向码头堆货,占满所有资源,让其他货主的船根本没有机会离港。
对于 HOL 阻塞、公平性和隔离问题,Jetty 模型都提供了统一且灵活的解决方案:当需要时,应用可以创建多个 Jetty。
- 缓解 HOL 阻塞:如果应用需要混合处理大的请求和大量小的请求,一个最佳实践是为它们使用不同的 Jetty,将大请求的”慢船”和小请求的”快艇”分流到不同的码头。
- 需要隔离:如果一个关键应用不希望它的数据收发被其他任何应用干扰,它可以创建自己专属的一对一 Jetty(一个 JFS/JFR 对),这在逻辑上部分回归了”连接”的抽象,用一个独立的”私有码头”来保证服务质量(QoS)。
- 需要公平性:一个服务如果需要公平地处理来自多个不同租户的请求,它可以为每个租户或每类请求建立不同的 Jetty,然后在应用层自己做轮询或调度。
这正是 Jetty 抽象的精妙之处:它提供了一个极致简单和可扩展的”无连接”模型作为默认选项,同时又把”需要多大程度的隔离和分流”这个选择权交还给了应用层。应用可以根据自己的需求,在”完全共享”和”完全隔离”之间,做出最适合自己的权衡。
Jetty 抽象下的单双边语义实现
Jetty 抽象可以用一套统一的队列模型,高效地实现单边和双边两种核心语义。
1. 单边内存语义 (One-Sided)
单边操作(如 RDMA Read/Write)的特点是,它像一次内存访问,只需要发起方提供地址、数据,而不需要对端应用 CPU 的参与。在 Jetty 模型中,这个流程被极度简化:
- 发起方应用将一个”写”请求(包含目标地址、数据等信息)提交到 JFS。
- UB 硬件从 JFS 取走请求,完成到目标的可靠传输。
- 目标端 UB 硬件直接将数据写入指定内存地址。
- 在发起方,UB 硬件在 JFC 中放入一个完成事件(CQE)。发起方应用通过检查 JFC 就知道操作已完成。
整个过程,发起方应用甚至不需要接收队列(JFR),因为它不”接收”任何应用层消息,只关心自己的操作是否”完成”。
2. 双边消息语义 (Two-Sided)
双边操作(如 Send/Receive)则需要两边应用的参与。JFR (Jetty for Receive) 的工作机制与 RDMA 的接收队列 (RQ) 相似,其核心是解耦消息的到达和应用对缓冲区的管理:
- 首先,目标端应用需要预先将若干”接收”请求提交到 JFR。每个接收请求都指向一块应用提供的内存缓冲区。
- 当发起方应用将一个”发送”请求提交到 JFS 后,UB 硬件会将其可靠地传输到目标端。
- 目标端 UB 硬件收到消息后,会从 JFR 中消耗一个预置的接收请求,将网络上收到的数据直接存入该请求对应的缓冲区中。
- 数据存入后,硬件会在 JFC (Jetty for Completion) 中放入一个完成事件,以此来通知目标端应用。这个完成事件会告知应用缓冲区地址、数据大小等信息。
- 目标端应用通过检查其 JFC,发现这个完成事件,就可以去处理相应缓冲区中的新消息了。
- 与此同时,在发起方,UB 硬件也会在其 JFC 中放入完成事件,通知”发送”操作已成功。
这种预先提交接收缓冲区的模式,引出了一个消息语义中的经典难题:如果接收方无法预知消息的大小,应该如何高效地管理缓冲区?为了应对这一挑战,Jetty 提供了灵活的缓冲区拆分与合并机制。例如,一个大的接收缓冲区可以被拆分用来接收多个小的消息;反之,一个大的消息也可以被”分散”到多个预先提交的小缓冲区中。
然而,这种灵活性带来了额外的复杂性,更可能引发不易察觉的性能问题。复杂的缓冲区管理不仅增加了应用层软件的负担,还可能因为一个逻辑消息对应多个硬件操作而干扰 JFC 的流控机制。因此,从设计理念和最佳实践出发,我们建议采用更简单、高效的一对一模式:为每个消息预备一个足够大的接收缓冲区。这再次印证了 UB 的核心设计哲学:双边消息语义是为高效的”通知”而生,而大块数据的传输应该交由单边内存语义来完成。
3. 高效的应用唤醒机制
最高性能的应用会持续轮询(Polling)JFC/JFR 来获取完成状态和新消息。但在很多场景下,应用可能处于休眠状态。如果每个完成事件都触发一次中断来唤醒 CPU,开销又太大了。Jetty 通过 JFC 和 EQ 的联动,实现了一种高效的异步通知机制:应用在提交请求时可以设置一个标志,请求硬件在事务完成后触发一个 Event。硬件会将这个 Event 放入 EQ 中,多个 Event 可以对应一次中断。应用进程被唤醒后,只需检查 EQ,就知道有”事件”发生,然后再去批量处理 JFC 和 JFR 中积累的多个完成消息。这就将”每消息一次”的潜在唤醒开销,变成了”每批消息一次”的开销,极大地提升了效率。
总而言之,Jetty 抽象是 UB 无连接设计哲学的基石。它用一个简单、无连接的”码头-队列”模型,取代了传统网络中复杂、有连接的状态机模型,将繁重的工作下沉到硬件,最终为上层软件提供了极致简洁、极致性能、极致可扩展的编程接口。
强事务序与弱事务序
在分布式系统中,顺序是保证一致性的核心,却也是性能的枷锁。
消息语义:挣脱字节流的枷锁
传统的网络通信,以 TCP 为代表,为我们提供了一个可靠的、点对点的字节流(Byte Stream)抽象。这是一个强大的模型,保证了数据不丢、不重、不乱。然而,当我们在一个连接中传递多个独立的业务”消息”时,这种严格的字节流序反而会成为性能瓶颈。如果承载第一个消息的数据包丢失,TCP 的可靠性机制会阻塞整个连接,直到这个包被重传成功。所有后续的、即使逻辑上完全独立的消息,也不得不原地等待。这就是著名的”队头阻塞”(Head-of-Line Blocking, HoL Blocking)。
现代网络协议的设计充分认识到了这一问题。无论是承载了 Web 未来的 QUIC 和 HTTP/3,还是为高性能数据中心设计的 UB,其核心变革之一就是用”消息语义”取代了”字节流语义”。通过在无连接的 UDP 或类似的底层协议之上构建多个独立的逻辑流,一个消息的传输问题不再会阻塞其他无关的消息。这为我们在更高维度上——即事务与事务之间——讨论顺序提供了必要的基础。
强序之梦:全局全序的诱惑与挑战
既然我们可以在逻辑上区分独立的事务,一个自然而然的终极理想便浮现出来:我们能否构建一个通信系统,将顺序保证从单一的点对点连接,扩展到整个网络?
这个想法最早的灵感,源于一个简单的物理直觉:网络中一个事件带来的影响,就像投入水中的石子激起的波浪,从一个网络节点(交换机或主机)传递到后续的节点。每一个数据包的转发,都像是波前(wavefront)的推进。如果我们能捕捉到这些”波”在整个网络中传播的先后顺序,不就能天然地为所有事件定义一个全局一致的顺序吗?
这是我在博士期间与左格非、白巍和张霖涛导师合作的研究课题(1Pipe: Scalable Total Order Communication in Data Center Networks)。这个工作的核心动机在于,如果网络能为所有节点提供一个”One Big Pipe”的抽象,让所有发送的事务(无论是单播还是多播)都在一个虚拟的全局序列中被严格排序,那么许多分布式系统难题都能有更高效、简单的解决方案,例如:
- 分布式事务:一次跨越多个节点的原子写入,可以被打包成一个全序”散射”(Scattering)消息,协议能保证所有节点都在同一”逻辑时刻”看到这次写入,从而天然地实现了原子性,无需复杂的两阶段提交或锁。
- 状态机复制:共识算法(如 Paxos/Raft)的核心就是对操作日志达成一个一致的顺序。如果网络本身就提供全序,复制(Replication)将只需要考虑故障问题,复杂度将极大降低。
- 内存一致性:分布式共享内存系统中,全局全序通信可以避免很多类型的 data hazard(数据读写顺序冲突),解决内存更新的顺序问题。
这个理想,本质上是试图在工程上实现 Leslie Lamport 在其划时代论文中,基于狭义相对论所描述的 happen-before
(→
) 关系的终极强化版——一个与所有因果关系都兼容的全局全序关系 (⇒
)。
然而,从 2018 年的首次投稿到 2021 年最终发表,我的 iPipe 这个工作历经五次投稿,其核心的挑战也暴露无遗:现实世界的网络充满了故障(Failure),而一个纯粹的全序系统在故障面前是极其脆弱的。 仅仅为消息分配一个全局唯一的序列号是不够的,因为你无法保证消息的可靠交付和原子性。如果一个发送者在分配了序号后、但在消息被所有接收者确认前崩溃,会发生什么?如果一个接收者永久下线,这次”原子”散射操作的完整性如何保证?为了解决这些问题,我们被迫在理想化的全序模型之上,增加了复杂的故障检测与恢复机制,系统的复杂性也随之剧增。
弱序之道:拥抱不确定性的新范式
这段艰难的科研经历让我深刻反思:我们是否真的需要一个如此昂贵、如此复杂的强因果、强顺序系统?
答案的曙光,意外地来自两个看似无关的领域:基础物理学与人工智能。一位同事告诉我,物理学界的前沿实验,如”量子开关”,已经揭示出宇宙在最精细的尺度上,其因果结构可能并非我们宏观世界里那样”坚固”。因果顺序本身可能是不确定的、叠加的,我们所体验到的确定性因果,或许只是微观概率世界在宏观尺度上的一个统计平均结果。
这个思想与现代 AI 系统的内在特性形成了奇妙的共鸣。今天,我们最大规模的计算负载——深度神经网络的训练与推理——其核心算法(如随机梯度下降)本身就是概率性的,并且天然地容忍甚至利用”噪声”。在一个本身就是概率性的系统中,由通信带来的微小乱序或延迟,仅仅是另一种可被算法消化的噪声而已。
既然宇宙的底层规则可能不是强因果的,而我们最重要的应用又可以容忍弱因果,那么,强行在不可靠的硬件之上构建一个完美的、强一致性的通信层,是否是一种不必要的、过度的工程设计?
这正是 Unified Bus 中”弱事务序”设计的哲学根基。UB 认识到,不同的应用场景对顺序和一致性的要求天差地别。因此,它不提供单一的、僵化的顺序模型,而是提供了一组分级的、可供应用按需选择的事务序原语。
UB 事务序:执行序与完成序
UB 将事务的顺序保证分解为两个正交的维度:执行序与完成序。
事务执行序 (Execution Order)
它定义了请求在 Target 端被执行的顺序,是保证一致性的核心。
- NO (No Order) :默认选项,性能最高。事务之间完全独立,Target 可以按任意顺序执行它们。适用于无状态的查询、独立的日志上传等场景。
- RO (Relaxed Order) :弱序的核心。它保证来自同一 Initiator 的、被标记为 RO 或 SO 的事务链,会按照发送的顺序执行。但它不会阻塞与此事务链无关的其他事务。它在保证”因果链内部不乱序”的前提下,最大化了并行度。
- SO (Strong Order) :强序的保证。被标记为 SO 的事务,必须等待从该 Initiator 发出的、在它之前的所有 RO 和 SO 事务都执行完毕后,才能开始执行。这提供了一个强序列点,适用于需要严格串行化的关键操作。
- Fence:一种特殊的屏障机制。它确保在它之前的所有事务(无论何种类型)都执行完毕后,在它之后的事务才能开始。它用于在不同的、逻辑独立的事务”批次”之间建立清晰的边界。
事务完成序 (Completion Order)
它定义了事务完成的通知(CQE)产生的顺序,与执行序解耦。这允许系统进行更灵活的优化。例如,事务可以按序执行,但乱序完成(比如写操作在持久化到日志后即可通知完成,无需等待数据落盘)。
通过将这些原语组合成不同的事务服务模式(如 ROI, ROT, ROL),UB 赋能给上层应用,让其根据自身的业务逻辑,在性能与一致性之间做出最明智的、细粒度的选择。对于需要强一致性的分布式数据库,可能会多使用 SO 和 Fence;而对于大规模的 AI 训练,绝大多数梯度更新都可以使用 RO 甚至 NO,从而将系统吞吐量推向极致。这套设计哲学,是对分布式系统”顺序”问题,从理论反思到工程实践的系统性回答。
Load/Store 与 Read/Write:内存访问的两种世界观
在 Unified Bus 的设计哲学中,对远程内存的访问并非只有一种方式,而是提供了两种核心的、互补的编程范式:一种是与处理器指令集深度融合的 Load/Store
,另一种是更为灵活、软件定义的 Read/Write
。这两种范式代表了两种不同的”世界观”,其背后是在编程模型、性能、一致性、硬件耦合度等多个维度上的深刻权衡。
两类范式:同步 Load/Store 与异步 Read/Write
要理解这两种范式的根本区别,我们首先要回到一个基本问题:一次远程内存访问,在应用和硬件层面,究竟是如何发生的?
什么是 Load/Store?
我们讨论任何系统语义时,首先要明确它是在哪个层次上实现的,否则就容易陷入鸡同鸭讲的辩经。Load/Store
语义的核心,在于它是否由处理器硬件指令直接支持。
- 在经典的图灵机模型中,Load 指令完成后,下一条指令才能开始。
- 在现代处理器的乱序执行模型中,Load 指令发出后,不相关的后续指令可以继续执行,但任何依赖该 Load 结果的指令会被硬件自动阻塞,直到数据返回。
- 在一些专用处理器(如 NPU)中,一条 Load 指令甚至可以搬运一个非连续的、巨大的数据块(如 Tensor 的一个切片)。
这些都属于 Load/Store
语义,因为它们都由硬件指令直接发起和管理。与之相对,那些由软件发起、运行时封装的内存访问(例如,软件构造一个工作请求,通过驱动发给网卡,再轮询完成队列),通常不被认为是 Load/Store
语义,即使它最终实现了数据的远程读取。
同步 vs. 异步
Load/Store
的本质是一种同步的内存访问模型,而传统的 RDMA Read/Write
则是异步模型的典范。
一次典型的异步 RDMA 写操作,其过程繁琐而漫长:
- 软件在内存中构造一个工作队列元素(WQE)。
- 软件通过”按门铃”(Doorbell)的方式通知网卡有新任务。
- 网卡从内存中将 WQE 读取到自己的片上内存。
- 网卡根据 WQE 中的信息,将用户数据从主机内存 DMA 到网卡。
- 网卡将数据打包成网络报文,发送到远端。
- 远端网卡接收报文,并将数据写入目标内存。
- 远端网卡返回一个确认消息。
- 发起端网卡收到确认后,在内存中写入一个完成队列元素(CQE)。
- 软件需要主动轮询(Poll)CQE,才能最终确认操作完成。
整个过程涉及多次 DMA 和复杂的软硬件交互。而在 UB 中,Read/Write
借鉴了 NVIDIA BlueFlame 等技术,允许在”按门铃”时附带少量信息,将 WQE 直接写入网卡的设备地址空间,从而省去了步骤 1 和 3,节约了两次 DMA 开销,但其异步交互的本质没有改变。
相比之下,一次同步 Load/Store 操作则极致简化:
- 应用执行一条
Load
或Store
指令。 - CPU 中的网络模块(或与之紧密集成的协处理器)直接将该指令转化为网络报文。
- 远端网络模块完成内存读写,并通过网络返回结果(或确认)。
- 发起端 CPU 的指令完成,流水线继续执行。
现代 CPU 的流水线机制可以很好地隐藏同步访问的部分延迟。虽然一个耗时过长的远程 Load
可能会阻塞部分流水线,降低并行度,但其端到端的延迟和软件开销远低于异步模型,尤其适合对延迟敏感的小数据块访问。
优劣总结
同步远程内存访问 (Load/Store)
- 优势: 过程简单,延迟极低;对应用透明,可直接用于内存扩展;小数据量访问效率高;可支持硬件缓存一致性。
- 劣势: 对硬件要求高(需 CPU 紧密集成);单次访问数据量小(通常是缓存行粒度);可靠性”爆炸半径”大(节点故障可能拖垮访问该节点内存的其他所有节点);大规模缓存一致性开销高。
异步远程内存访问 (Read/Write)
- 优势: 可灵活指定访问数据量,大数据传输吞吐高;对硬件要求低,解耦性好;可通过软件捕获异常,故障隔离性好。
- 劣势: 过程复杂,延迟较高;对应用不透明,需要显式编程;无硬件缓存一致性,需软件保证。
正是因为这两种模式各有千秋,UB 协议才选择同时提供它们,让开发者能根据场景按需选择。
远程内存寻址
提供了两种编程范式后,下一个核心问题是:如何为远程内存”命名”和”定位”,即寻址?UB 的内存管理机制,正是围绕着如何高效支持 Load/Store
和 Read/Write
这两种模式而构建的。
在 UB 的世界里,内存的基本管理单元是内存段(Memory Segment)。一个内存段是一段连续的物理内存。当一个节点(Home)希望将其部分内存共享给其他节点(Initiator)时,它会创建一个内存段,并为其分配一个唯一的标识符(TokenID)。Initiator 要想访问这段远程内存,就必须先从 Home 获取这个内存段的信息,包括 TokenID、基地址(UBA)和大小(Len)。
获取了这些信息后,Initiator 面临一个关键选择:如何将这个远程内存地址”翻译”成自己可以理解和使用的地址?业界和学术界探索了多种路径,各有优劣:
- 物理内存统一编址 (Globally Shared Physical Memory)
这种方式最为简单直接,常见于传统的紧耦合 HPC 系统。在整个系统中,所有节点的物理内存都被映射到一个全局统一的物理地址空间。任何节点访问一个地址,硬件都能直接解析到它属于哪个节点的哪块物理内存。这种模型的优点是硬件实现简单。但其致命缺点是扩展性极差。随着节点数量增多,维持一个全局一致的物理地址视图变得异常困难和昂贵。
- 网络地址 + 远程虚拟地址 (Network Address + Remote VA)
这是一种更为灵活和可扩展的方案。访问一个远程内存地址需要一个”二元组”:目标节点的网络地址,以及该内存在目标节点上的虚拟地址。这种方式将地址空间解耦,每个节点维护自己的地址空间,扩展性非常好。UB 协议中的 read
和 write
事务就支持这种访问方式。
然而,这种网络地址 + 虚拟地址的二元组通常非常长(例如需要 128 位),无法被现有的 CPU 指令直接作为内存地址来寻址。发起一次远程读写,需要 CPU 执行专门的指令,将这个长地址和操作类型一起打包成请求,交给网卡硬件去处理。这种方式的另一个重要特征是,它本质上是一种非缓存(Non-cacheable)的访问模式。每次读写都直达对端内存,数据不在本地缓存。这样做的好处是模型简单,完全不存在缓存一致性问题,因为根本就没有缓存。但缺点也很明显,即每次访问都需要承受完整的网络延迟。
- 映射到本地虚拟地址 (Mapping to Local VA)
这是 UB 协议为追求极致性能而提供的核心机制,由 load
和 store
指令支持。在这种模式下,Initiator 会将获取到的远程内存段,通过本地的内存管理单元(UBMMU),映射到自己进程的虚拟地址空间中。一旦映射完成,CPU 就可以像访问本地内存一样,使用标准的 load
和 store
指令来访问这段远程内存。
这种方式的性能优势是巨大的,因为它将远程内存”本地化”了,使得 CPU 流水线可以无缝地处理远程访问,无需特殊的指令和软件开销。更重要的是,这种模式天然地支持缓存(Cacheable)。当 CPU load
一个远程地址时,数据可以被缓存到本地 Cache 中,后续的访问就可以在极低的延迟下完成。
当然,引入缓存也带来了新的挑战:如何保证缓存的一致性?UB 协议为此设计了精巧的缓存一致性机制。通过在内存段映射时设置不同的权限(如只读或读写),系统可以为后续的缓存一致性管理留下充足的设计空间。例如,一个被多个节点以只读方式映射的内存段,其缓存管理就相对简单;而一旦允许写入,就需要更复杂的机制来确保数据的一致性。
综上所述,UB 的内存管理机制提供了一个分层的、灵活的解决方案。它既提供了简单、无需考虑一致性的 read/write
模式,也提供了与 CPU 指令集深度融合、支持缓存的 load/store
高性能模式,让应用可以根据自身的需求,在易用性、性能和一致性之间做出最合适的选择。
缓存一致性
当多个节点可以将同一段远程内存映射到本地并进行缓存时,缓存一致性就从一个”可选项”变成了”必答题”。如果管理不当,不同节点缓存中的数据副本就可能出现不一致,导致程序读到脏数据,从而产生灾难性的后果。UB 协议作为一个旨在提供内存级语义的系统,必须提供清晰可靠的缓存一致性方案。
设计缓存一致性协议,本质上是在性能、复杂度和一致性强度这三个维度之间进行权衡。业界已经发展出多种不同强度和实现方式的一致性模型,以下探讨几种典型的方案,并结合 UB 的设计理念进行分析:
- 任意节点、强一致性(动态共享列表)
这是最理想化的模型:允许多个节点同时缓存一份数据,并保证其访问体验与访问本地多核处理器的内存完全一致(即强一致性)。当一个节点修改数据时,协议需要通过类似”广播”或”多播”的方式,要么使其余所有节点的缓存副本失效(Invalidate),要么将更新同步给它们(Update)。这种模型的关键挑战在于维护一个动态的共享者列表(Sharer List)。由于任何节点都可能随时加入或退出,这个列表的大小和成员都是不固定的,硬件要高效地管理这样一个动态列表,复杂度非常高,扩展性也面临挑战。
- 多读单写(Multiple Readers, Single Writer)
这是实践中最为常见和经典的一致性模型,也是现代多核 CPU 中 MSI/MESI/MOESI 等协议的基石。它规定,在任何时刻,一段数据可以被任意多个节点持有只读缓存(Read-only Cache),但最多只能被一个节点持有可写权限(Write Permission)。当某个节点希望写入数据时,它必须先获得唯一的写权限,而获得该权限的前提是,网络中所有其他只读缓存副本都必须被设置为无效(Invalidate)。UB 协议文档中描述的 Ownership(所有权)概念和 Invalid/Read/Write 三种状态的转换,正是这一思想的体现。这种模型实现相对简单,在读多写少的场景下性能很高,是性能和复杂度之间一个很好的平衡点。
- 独占访问/所有权迁移(Ownership Migration)
这是”多读单写”模型的一个特例。在这种模式下,任何时候只允许一个节点访问(缓存)某个内存段。当一个节点(Borrower)需要访问时,它会从原始持有者(Home)那里”借走”所有权,成为新的 Owner。在此期间,原有的 Home 节点将暂时失去对这段内存的访问权,直到 Borrower 将其”归还”。这种模型的实现最为简单,因为它完全避免了多个副本之间的一致性问题。它适用于内存借用场景,即 Home 节点把空闲的内存 “出租” 出来,组成一个大的内存池,其他内存不足的节点从内存池中索要内存,用以弥补本地内存的不足。
- 有限节点、强一致性(固定共享列表)
这是对第一种理想模型的简化和妥协。它同样提供强一致性,但限制了能够同时共享一份数据的节点数量。因为共享者数量有上限,硬件可以用一个固定大小的列表来维护,从而大大降低了设计复杂度。然而,这种模型的实用性不强,因为它在应用层引入了一个不自然的限制,难以适应通用和动态变化的计算需求。
- 软件管理一致性(Explicit Cache Management)
这是一种将一致性维护的责任部分或全部交给软件的方案。硬件依然提供缓存机制以加速访问,但它不自动保证不同节点间缓存的一致性。当应用需要确保自己读到最新数据时,必须由软件显式地执行 refresh cache
(或 invalidate
)操作,主动丢弃本地可能过期的缓存。当应用修改了数据,希望对其他节点可见时,也必须显式地执行 write back
(或 flush
)操作,将本地缓存写回到主内存。这种模型给了软件最大的灵活性,但对程序员的要求极高,容易出错。
- 非缓存模式(Non-cacheable)
这是最简单直接的”一致性”方案:没有缓存,自然也就不需要考虑一致性。如前文所述,UB 的 read/write
事务就属于这种模式。每次访问都直达目标主内存,确保了读取到的永远是最新数据。它的代价是需要应用程序自行实现数据搬移,把数据从 Home 节点搬移到本地,才能享受缓存带来的访问效率提升。
UB 协议的设计,正是在上述多种可能性中寻求最佳平衡的成果。它以”多读单写”的所有权模型为核心,为 load/store
缓存访问提供了强大的硬件一致性保证,同时又保留了 read/write
这一非缓存的通道作为补充,从而让不同的应用都能找到最适合自身需求的一致性与性能平衡点。
内存池的杀手级应用:KV Cache
当我们五年前首次构思基于 UB 的内存池时,我们手中握着一个强大的解决方案,却一直在苦苦寻觅一个真正与之匹配的问题。我们设想,通过 UB,可以汇集上千台服务器的内存,构建出一个前所未有的、统一的巨大内存池,并且池中的任何数据都能被以接近本地内存的超低延迟访问。这在技术上是激动人心的,但一个现实的问题始终萦绕在我们心头:”究竟什么样的应用,才需要这样一个规模庞大、性能极致的共享内存池?”
LLM 推理服务的爆发,带来了 KV Cache 的挑战。LLM 在生成文本时,需要缓存海量的中间状态(即 KV Cache),其大小动辄数十上百 GB,远超单张 GPU 显存的极限。更关键的是,这部分数据在每个 token 的生成过程中都必须被高频访问,对延迟和带宽极为敏感。突然之间,我们五年前提出的所有设想——巨大的容量、极低的延迟、高效的共享——都在 KV Cache 这个问题上找到了完美的用武之地。
1. Prefill-Decode 分离
LLM 处理一个请求分为两个阶段:
- Prefill 阶段:接收用户输入的 Prompt、对话历史或 Agent 工具调用轨迹,并行计算出所有 token 的 KV Cache。这是一个计算密集型(Compute-bound)的过程。
- Decode 阶段:逐一生成新的 token。每生成一个 token,都需要读取完整的 KV Cache(包含提示语和之前所有已生成的 token)。这是一个内存带宽密集型(Memory-bound)的过程。
由于这两个阶段的计算特性截然不同,大规模 LLM 推理框架普遍采用了 Prefill-Decode (PD) 分离的调度策略。系统会将大量的 Prefill 请求聚合成一个大的批次进行计算,同时将 Decode 请求聚合成另一个批次。这种分离调度可以显著提高 GPU 的利用率和系统整体的吞吐量。
2. Prefix KV Cache
在很多应用场景中,不同的用户请求常常包含相同的 “前缀”(Prefix)。例如,在多轮对话和 Agent 中,后续的请求完全包含了之前的对话历史或工具调用历史。
为这些相同的前缀重复计算 KV Cache 是巨大的浪费。Prefix Caching 技术应运而生。其核心思想是,将计算好的前缀 KV Cache 存储在一个全局的内存池中。当一个新的请求到来时,系统会检查其前缀是否与缓存中的某个条目匹配。如果匹配,则直接从内存池中找到这份共享的 KV Cache,然后从前缀末尾继续计算即可。这极大地降低了首个 token 的生成延迟(Time to First Token, TTFT),并节省了可观的计算资源。
这种基于内存池的 Prefix Caching 机制,本质上就是一种跨请求的计算结果复用。UB 协议所倡导的全局内存池和低延迟内存借用,为实现一个高效、跨服务器的全局 KV Cache 池提供了理想的硬件基础。
从更深层次看,KV Cache 的成功,或许是计算机系统领域对 AI 领域做出的最核心的贡献之一。Transformer 模型中的注意力机制可以被看作是一种新颖的、可微分的”键值存储”(Key-Value Store)。在这个范式中,查询向量(Query)是我们要查找的”键”,而上下文中的所有词元都提供了自己的”键”(Key)和”值”(Value)。与传统系统里通过哈希表进行精确、离散匹配的 map[key] -> value
不同,注意力机制进行的是一种模糊、连续的”软”匹配(softmax 中的 soft 正是这个意思)。它用当前 Query 与所有 Key 计算相似度(注意力分数),然后按此分数对所有的 Value 进行加权求和。这相当于一次性地、按相关性程度”读取”了数据库中的所有内容。
总结:Load/Store 与 Read/Write
综上所述,UB 提供的 Load/Store
与 Read/Write
绝不是冗余的功能,而是不同场景必需的两种抽象。
Load/Store
提供了极致的低延迟和编程透明性,将远程内存无缝融入了处理器的原生指令集,是构建高性能、细粒度共享内存应用的利器。但它也带来了硬件实现的复杂度。Read/Write
则提供了一种更为传统和灵活的异步访问模型,它解耦了软硬件,简化了一致性模型,更适合大块数据的搬运和对延迟不那么敏感的场景。
URMA:统一远程内存访问
行文至此,我们已经探讨了 UB 设计背后的诸多关键决策:从打破 CPU 瓶颈的”对等架构”,到解决大规模扩展性难题的”Jetty 无连接模型”,再到为性能优化的”弱事务序”和”Load/Store 语义”。
URMA(Unified Remote Memory Access,统一远程内存访问),是将所有这些设计哲学融为一体的顶层概念,由分布式与并行软件实验室主任谭焜博士提出。URMA 正是 UB 协议为上层应用提供的统一编程抽象和核心语义的集合。
URMA 的诞生,源于对未来计算模式的深刻洞察。在未来的数据中心和高性能计算集群中,CPU、GPU、NPU 等异构算力单元将以对等的方式协同工作,共同处理复杂的计算任务。为了完全释放这种异构算力的潜力,底层的通信协议必须满足几个严苛的诉求:
- 异构算力直接通信:必须允许不同类型的算力单元以对等的方式直接通信,绕开传统的主从架构瓶颈,从而在细粒度的任务上实现高效的并行与协作。
- 极致的可扩展性:协议必须能高效地支持从单节点内到超大规模集群的通信,轻松应对数以万计节点的互联需求。
- 最大化的网络效率:协议需要内建灵活的路由和传输机制,例如通过多路径和乱序传输,来充分利用昂贵的数据中心网络带宽,保障业务的实时性。
URMA 正是为这三大诉求量身打造的答案。它旨在高效、可靠地完成任意两个 UB 实体(UB Entity)之间的通信,无论是单边的内存访问(DMA),还是双边的消息收发。我们在前文中所讨论的那些关键特性,最终都在 URMA 的设计中得到了体现:
- 平等的访问 (Peer-to-Peer Access) :这是 URMA 的基石。任何异构算力设备都可以通过 URMA 实现免 CPU 介入的直接通信,呼应了我们最初对”对等架构”的设想。
- 天生的无连接 (Inherently Connectionless) :URMA 通过 Jetty 抽象,让应用之间可以直接复用 UB 传输层提供的可靠服务,无需建立和维护端到端的连接状态。这从根本上解决了传统 RDMA 的可扩展性问题,是其能够支撑超大规模部署的关键。
- 灵活的事务序 (Weak Ordering) :URMA 允许应用根据自身需求配置任务的保序行为。它默认允许乱序执行和乱序完成,这不仅避免了”队头阻塞”问题,更释放了底层硬件进行多路径传输和并行处理的巨大潜力,从而显著提升了端到端的执行效率。
总而言之,URMA 不仅仅是一套 API 或协议规范,它更代表了一种面向未来的、统一的异构计算通信范式。它将总线的高性能与网络的灵活性融为一体,通过一系列创新的设计,为上层应用屏蔽了底层硬件的复杂性,最终提供了一个简单、高效且极具扩展性的编程接口。这正是 Unified Bus “统一总线”这个名字的最终奥义所在。
当然,如同任何一次范式的转移,URMA 的统一抽象并非没有代价。它将一部分过去由操作系统和软件处理的复杂性(如内存管理、一致性)下沉到了硬件,带来了巨大的硬件设计挑战。同时,它也向上层应用暴露了更多的选项和责任,例如对事务序、负载均衡策略的选择。一个新范式的胜利,不在于它解决了所有旧问题而没有引入任何新问题,而在于它所解决的核心矛盾——在 UB 的案例中,即’性能’与’规模’的矛盾——是当前时代最迫切、最重要的问题。
拥塞控制
拥塞控制的历史,本质上是一部我们与”队列”这个魔鬼的斗争史。
最早,TCP/IP 的设计者认为网络是”尽力而为”的,拥塞的信号就是丢包。因此,TCP 采用了经典的”加性增窗,乘性减窗”(AIMD)算法:没丢包就慢慢增大发送窗口,一旦丢包,就猛烈地把窗口砍半。这个模型在广域互联网上取得了巨大成功,但它的一个根本问题是,它是一个”事后”控制。它依赖队列被填满、最终导致丢包来感知拥塞,这必然导致两个问题:1)高延迟,即所谓的”Bufferbloat”(缓冲区膨胀);2)网络利用率剧烈震荡。
为了解决这个问题,人们提出了主动队列管理(AQM),其代表是 RED (Random Early Detection)。RED 的思想是,不要等到队列满了再丢包,而是在队列长度开始增加时,就以一定概率随机丢弃数据包,提前向发送方发出拥塞信号。这在一定程度上缓解了 Bufferbloat,但”丢包”作为一个拥塞信号,依然过于粗暴。
一个更温和的方案是显式拥塞通知(Explicit Congestion Notification, ECN)。ECN 允许路由器在发现队列开始积压时,在包头上打一个标记,而不是丢弃它。发送方收到这个标记后,就知道网络出现了拥塞,从而主动降低发送速率。ECN 避免了不必要的丢包和重传,是现代网络拥塞控制的标配。
然而,在数据中心这种追求极致低延迟和高吞吐的场景下,这些基于 TCP 的拥塞控制方案依然不够精细。特别是对于 RDMA 这种无连接、对丢包极度敏感的应用,需要更快速、更精确的拥塞控制。RoCEv2 网络为此设计了 DCQCN(Data Center Quantized Congestion Notification)。DCQCN 结合了 ECN 和基于速率的控制,交换机标记拥塞后,网卡会快速地、按一定的步长降低发送速率,实现了更快的收敛和更低的队列占用。
UB 的 C-AQM (Confined Active Queue Management) 进一步将这种精细化控制推向了极致。DCQCN 依然是一种”先拥塞、再降速”的被动模式,而 C-AQM 的核心思想是”按需分配、主动授予”,目标是实现”近似零队列”。这背后最大的优势,正是华为作为端到端(网卡+交换机)网络设备提供商的”端网协同”能力。
C-AQM 的工作机制体现了这种协同的精妙之处:
- 发送端(UB Controller)主动请求:发送端在发送数据时,可以通过包头中的
I
(Increase)位置 1,来向网络请求增加带宽。 - 交换机(UB Switch)精确反馈:当交换机收到这个请求后,它会评估自己出口端口的拥塞状况。如果认为增加带宽会导致拥塞,它不仅会在包头中置位
C
(Congestion) 位来拒绝请求,还会在Hint
字段中,给出一个建议的带宽值。这个Hint
值是交换机根据自己精确的队列和带宽占用情况计算出来的,它告诉发送端:”你不能再增加了,你应该把速率调整到这个建议值”。 - 发送端快速响应:发送端收到这个包含了精确
Hint
值的反馈后,就可以立即将自己的发送速率调整到交换机建议的水平。
这个过程,就像一个聪明的交通指挥系统。司机(发送端)想要加速,先通过信号灯(I
位)询问前方的交警(交换机)。交警根据整个路口的实时车流情况,不仅会亮红灯(C
位)告诉司机”不行”,还会通过对讲机(Hint
字段)直接告诉司机”保持时速 30 公里”。
通过这种”请求-精确反馈”的闭环,C-AQM 使得发送端的发送速率与网络能够提供的服务能力被精确地匹配起来,数据包”随到随走”,交换机的队列始终维持在一个极低的、接近于零的水位。这不仅彻底消除了 Bufferbloat 带来的高延迟,也最大化了网络的有效吞吐。这种近似零排队的设计理念,是 UB 实现微秒级端到端延迟的关键基石之一。
可靠传输
在构建任何一个可靠的网络协议时,如何处理丢包都是核心议题。TCP/IP 的教科书式方案——“慢启动、拥塞避免、快重传、快恢复”——早已深入人心。然而,在数据中心网络中,为了最大化带宽利用率,普遍采用了等价多路径路由(ECMP)。流量被分散到多条物理路径上传输,这不可避免地会导致数据包到达顺序与发送顺序不一致。一个根本性的矛盾便浮现出来:我们如何能在一个充满了”有序的乱序”的网络中,精确地判断出丢包?
然而,传统的快速重传机制,其核心逻辑是”收到三个重复的 ACK 就认为一个包丢了”。这个假设在单一路径上是基本合理的,但在 ECMP 环境下,这种”乱序”会轻易地骗过这个机制,导致大量的伪重传(Spurious Retransmission)。网络本身没有丢包,只是有些包抄近路先到了,协议却误以为丢包,疯狂发起不必要的重传,这不仅浪费了宝贵的带宽,甚至可能因为引入了额外的流量而导致真正的拥塞。
UB 的负载均衡机制
与传统网络中依赖 ECMP 哈希”听天由命”式的负载均衡不同,UB 将选择权交给了应用,让其根据自身对性能和顺序性的需求,在不同的负载均衡粒度之间做出明智的权衡。UB 支持两个层次的负载均衡机制上:
1. 事务级负载均衡:基于 TPG 的”车队”模式
UB 引入了 TPG (Transport Protocol Group) 的概念。我们可以将一个 TPG 想象成一家物流公司,它负责将一批批的”货物”(即事务)从 A 点运到 B 点。为了提高运力,这家公司可以同时使用多条高速公路,每一条高速公路就是一个 TP Channel。
当一个事务(例如一次大的 RDMA Write)需要发送时,TPG 会为它选择一条 TP Channel。一旦选定,这个事务的所有数据包(TP Packet)都会在这条固定的”高速公路”上传输。这种模式,就像一个庞大的车队,所有车辆都保持在同一条车道上行驶。
这种事务级负载均衡的巨大优势在于简单性和有序性。由于同一事务的所有数据包都在同一条路径上,它们天然就是有序到达的,从根本上避免了包级乱序的发生。这使得上层的可靠传输协议可以放心地使用最高效的快速重传机制,因为任何一个”重复 ACK”的信号,都极大概率指向一次真正的丢包,而非乱序。这是一种安全、稳定、易于管理的并行策略,适用于大多数需要可靠传输的场景。
2. 包级负载均衡:极致性能的”赛车”模式
对于那些追求极致网络利用率和最低延迟的应用,UB 提供了更为激进的包级负载均衡机制。在这种模式下,系统允许同一个事务的数据包,被”打散”到多个不同的 TP Channel,甚至通过修改包头中的 LBF 字段,在交换机层面被动态地导向不同的物理路径。
这就好比一场公路赛车,为了最快到达终点,每辆赛车(TP Packet)都可以自由选择最不拥堵的车道,动态超车、变道。
这种模式能最大程度地”填满”网络中的所有可用带宽,实现无与伦比的吞吐量。然而,它也带来了一个必然的”副作用”:乱序。后发的数据包完全有可能因为选择了一条更快的路径而先于先发的数据包到达。
多路径下的丢包重传
在多路径传输下,对”丢包”的判断必须更加审慎和智能。强行套用一个”一刀切”的方案是行不通的。因此,我们没有设计一个万能的重传算法,而是将重传的决策权交还给用户,由两个维度的选择构成:
重传范围:是”一人犯错,全体罚站”,还是”精准打击”?
- GoBackN (GBN) :这是一种简单而经典的策略。一旦检测到丢包,发送方会从丢失的那个包开始,重传它以及它之后所有已发送的包。这种方式的好处是实现简单,接收方需要维护的状态极少。但在高延迟、高带宽、高丢包率的网络中,它可能会重传大量本已正确送达的数据,造成效率低下。
- 选择性重传 (Selective Retransmission) :这是一种更精细化的策略。发送方只重传那些被确认丢失的包。接收方需要维护一个更复杂的状态(例如一个 bitmask),来告诉发送方哪些包收到了,哪些没收到。这种方式效率最高,但实现也更复杂。
触发机制:是”火急火燎”,还是”三思后行”?
- 快速重传 (Fast Retransmit) :类似于 TCP 的机制,通过冗余的确认信息(例如 UB 中的错误应答)来快速触发重传,无需等待一个完整的超时周期。它的优点是响应快,能显著降低丢包时的延迟。但如前所述,它对乱序非常敏感。
- 超时重传 (Timeout Retransmit) :这是最保守、最可靠的机制。发送方为每个发出的包启动一个计时器,如果超过预设时间(RTO)还没收到确认,就认为包丢了并发起重传。它的优点是能最终兜底所有丢包场景,并且不受乱序影响。缺点是 RTO 的计算通常比较保守,等待超时的过程会引入较长的延迟。
通过将这两个维度的策略进行组合,UB 为用户提供了四种重传模式,以适应不同的网络场景:
重传算法 | 适用场景 | 网络丢包率 | 设计思考 |
---|---|---|---|
GoBackN + 快速重传 | 单路径传输,如逐流负载均衡 | 非常低 | 这是最经典、最高效的模式。当网络路径稳定、乱序风险极低时,我们应该用最快的速度去修复极少数的丢包。 |
GoBackN + 无快速重传 | 多路径传输,如逐包负载均衡 | 非常低 | 当我们知道网络本身会引入大量乱序时(如 ECMP),就必须关掉对乱序敏感的快速重传,完全依赖超时来保证可靠性,避免伪重传风暴。 |
选择性重传 + 快速重传 | 单路径传输,如逐流负载均衡 | 低 | 在稳定的单路径网络中,如果丢包率开始变得不可忽略,选择性重传相比 GBN 能带来明显的效率提升,避免不必要的重传。 |
选择性重传 + 无快速重传 | 多路径传输,如逐包负载均衡 | 低 | 这是最复杂但适应性最强的模式。它为那些既存在多路径乱序、又有一定丢包率的复杂网络环境,提供了最高效、最稳健的可靠传输方案。 |
重传与事务序的协同
前文我们讨论了 UB 如何通过”弱事务序”来打破不必要的顺序枷锁,最大化并行传输的效率。一个自然而深刻的问题随之而来:如果系统允许事务乱序执行,那么一个”旧”事务的重传包,与一个”新”事务的包,它们之间是什么关系?
我们有个核心的设计哲学:传输层的可靠性(Reliability)与事务层的顺序性(Ordering)在设计上是正交解耦的。
- 传输层的使命:它的世界里只有 TP Packet 和它们的序列号(PSN)。它的唯一目标,就是通过 GBN 或选择性重传等机制,确保一个事务所对应的 所有 TP Packet,最终能完整、无误地从发送端(Initiator)的传输层,交付给接收端(Target)的传输层。它负责处理网络中的物理丢包,实现”数据必达“。
- 事务层的使命:它的世界里只有事务(Transaction)以及它们的顺序标记(NO, RO, SO)。它的工作,是在传输层确认收齐了一个事务的所有数据之后,才开始的。它根据事务的顺序标记,来决定这个刚刚”凑齐”了所有零件的事务,应该何时被执行。它负责处理业务逻辑上的依赖关系,实现”按需保序“。
让我们通过一个具体的场景来理解这种解耦如何工作:
发送:Initiator 先后发送了两个事务:
- 事务 A (RO):被切分为三个包 A1, A2, A3。
- 事务 B (RO):被切分为两个包 B1, B2。
丢包:在网络传输中,A2 不幸丢失。而 A1, A3, B1, B2 都顺利到达了 Target。
传输层响应:Target 的 UB 传输层开始工作。
- 对于事务 B,它收到了 B1 和 B2,通过 PSN 检查发现数据完整。于是,它将重组好的事务 B 向上交付给事务层,宣告任务完成。
- 对于事务 A,它收到了 A1 和 A3,但通过 PSN 或 Bitmap 发现 A2 不见了。它会默默地将 A1 和 A3 缓存起来,同时通过 TPNAK 或等待超时等机制,通知 Initiator 的传输层:”A2 丢了,请重发。”
事务层决策:此时,Target 的事务层也开始工作。
- 它收到了从传输层递交上来的、完整的事务 B。
- 它检查事务 B 的顺序标记,是 RO(Relaxed Order)。这意味着 B 无需等待在它之前发送的任何事务。
- 因此,事务层会立刻将事务 B 投入执行,完全无视此刻还在苦苦等待重传包 A2 的事务 A。
重传与最终执行:
- 稍后,重传的 A2 数据包终于抵达。
- Target 的传输层将它与之前缓存的 A1, A3 成功合体,重组出完整的事务 A,并交付给事务层。
- 事务层收到事务 A,检查其 RO 标记,同样将其立即投入执行。
在这个过程中,一个”旧”事务(A)的丢包和重传,完全没有阻塞一个”新”事务(B)的执行。这就是弱事务序与可靠传输协同工作的威力。
那么,强顺序(SO)又会如何改变这一切呢?
如果在上述场景中,事务 B 被标记为 SO (Strong Order) ,那么在第 4 步”事务层决策”时,情况将完全不同。当事务层收到完整的事务 B 后,它会检查其 SO 标记,并意识到自己必须等待所有在 B 之前的事务(即事务 A)执行完毕。因此,即使 B 的所有数据都已备齐,事务层也只能让它”原地待命”,直到重传的 A2 到达、事务 A 被成功执行后,事务 B 才能被执行。
总结而言,UB 的这套解耦设计,实现了一种极致的效率:
- 网络层面的问题,就在网络层面解决:传输层可以积极地使用各种高级技巧(如选择性重传)来最高效地对抗丢包,而不用担心自己的行为会干扰到上层业务的逻辑顺序。
- 业务层面的顺序,由业务自己决定:事务层则可以从网络细节中解脱出来,专注于根据应用的真实需求来决定是否需要等待、需要何种强度的顺序保证,从而避免了不必要的”队头阻塞”。
这种清晰的责任划分,使得系统在默认情况下获得了最大的并行度和性能,同时又为需要强一致性的应用保留了构建严格顺序的能力。这是对现代数据中心网络复杂性的一种优雅且高效的回答。
死锁避免
数据链路层的死锁避免
在任何一个保证无损(lossless)的通信网络中,死锁都是一个必须面对的幽灵。当网络采用基于信用的流控(Credit-based Flow Control)或背压机制(Back Pressure)时,如果资源(如缓冲区)的依赖关系构成了环路,就可能出现所有节点都在等待对方释放资源,而自己又占着资源不放的”循环等待”状态,这就是死锁。
一个典型的场景是:网络中有四个交换机,构成了一个环形依赖。UB Switch 1 的出口缓冲区满了,因为它想到达 UB Switch 2 的数据发不出去;Switch 2 的缓冲区也满了,因为它在等待 Switch 3;Switch 3 在等待 Switch 4;而 Switch 4 又在等待 Switch 1。所有的缓冲区都被占满,数据无法流动,整个系统陷入停滞。
为了避免这种灾难性的情况发生,必须在设计上打破这种循环等待的条件。在 UB 设计中,采用以下几种经典的死锁避免方案:
- 基于路由的死锁避免
这种方法从根本上入手,通过设计一个无环的路由算法来保证死锁不会发生。一个经典的例子是基于生成树的”上/下路由”(Up/Down Routing)。首先在网络拓扑中确定一个根节点,并构建一个生成树。路由规则被限制为:数据包可以从”下方”节点路由到”上方”节点(即更靠近根节点的节点),也可以从”上方”节点路由到”下方”,但绝不允许在经过一个”上方”节点后,再路由到一个”下方”节点。这个简单的限制有效地打破了任何潜在的路由环路,从而避免了死锁。这种方法的优点是简单高效,但缺点是可能无法充分利用网络中的所有可用路径,牺牲了一定的灵活性和性能。
- 虚拟通道(Virtual Channels/Lanes, VC/VL)
这是现代高性能网络(如 InfiniBand)中最常用、最灵活的死锁避免机制。它的核心思想是,将每个物理链路(Physical Link)分割成多个逻辑上独立的虚拟通道(VC)。每个 VC 都有自己独立的缓冲区资源。
虽然物理拓扑可能存在环路,但我们可以通过精心设计 VC 的使用规则,构建出一个无环的”VC 依赖图”。例如,我们可以将 VC 分成不同的”层级”,并规定数据包只能从低层级的 VC 跳转到高层级的 VC。当数据包在一个环路中绕行时,它每次跳转都必须进入一个更高层级的 VC。由于 VC 的层级数量是有限的,数据包最终会因为无法找到更高层级的 VC 而无法继续,从而打破了循环等待。VC 机制将资源依赖从物理链路层面解耦到了更细粒度的虚拟通道层面,极大地提升了路由的灵活性和网络资源的利用率。
- 基于超时的死锁恢复
与前两种”避免”策略不同,这是一种”检测并恢复”的策略。系统为每个数据包设置一个计时器。如果一个数据包在缓冲区中停留的时间超过了某个阈值,系统就认为可能发生了死锁。一旦检测到死锁,就会采取措施来打破它,最简单粗暴的方法就是丢弃一个或多个”老”数据包,释放它们占用的缓冲区,让其他数据包可以继续前进。这种方法通常作为其他死锁避免机制的补充,是一种最后的保险措施,因为它破坏了网络的无损特性。
内存访问中的死锁避免
在 UB 这样复杂的系统中,一次看似简单的内存访问,背后可能隐藏着一系列复杂的连锁反应。一次原生的内存操作(例如一条 Load
指令)可能会触发次生的内存操作(例如处理缺页、内存借用时的地址转换等)。当这些原生和次生操作之间形成资源或流程上的循环依赖时,系统就可能陷入死锁。
以下是三种典型的内存访问死锁场景:
- 内存池化借用 (Memory Pooling/Borrowing) :在对等架构下,每个 UBPU 既是”内存使用方”,也是”内存提供方”。当两个节点互相借用对方的内存时,就可能形成死锁。例如,节点 A 借用了节点 B 的内存,同时节点 B 也借用了节点 A 的内存。当 A 和 B 都需要通过
Writeback
更新对端内存,并等待对方的确认(TAACK)时,如果双方的确认消息因为资源占用的原因被互相阻塞,就会导致死锁。 - 页表访问 (Page Table Access) :当一次内存访问需要通过 UMMU 进行地址转换时,如果 UMMU 的页表项本身存储在远程的、被借用的内存上,读取页表项的这个次生操作,就需要再次通过相同的端口发起一次远程内存访问,这就可能与原生的内存访问形成资源竞争,导致死锁。
- 缺页处理 (Page Fault Handling) :UB 支持动态内存管理,这意味着内存访问可能触发缺页(Page Fault)。处理缺页的过程,可能需要访问外部存储,或者从其他 UBPU 获取数据。如果处理缺页的这个次生操作,与它所服务的原生操作之间,在硬件电路上形成了依赖,也可能导致死锁。
为了应对这些复杂的死锁场景,UB 提供了一套组合拳:
- 请求重试:允许操作在资源受限时失败并由上层重试。
- 虚拟通道隔离:通过为不同类型的流量(如原生访问、页表访问、缺页处理)分配不同的虚拟通道,在硬件层面打破资源依赖环路。
- 事务类型区分:对不同类型的事务进行区分,施加不同的处理策略。
此外,实现者也可以通过更简单的策略,例如保证页表总是本地化存储,来从根本上规避某些死锁场景。
消息通信中的死锁避免
UB 的双边消息通信,例如 Jetty 上的 Send/Receive,是以队列为基础的。当队列资源不足时,消息通信就会被阻塞。如果不同 UBPU 上的消息队列,因为互相发送消息而形成首尾相连的依赖环路,就可能导致死锁。
例如,节点 A 和节点 B 都在向对方大量发送 Send
事务。由于外部可能有大量其他节点也在向 A 和 B 发送请求,导致 A 和 B 的接收队列(Jetty)都被打满。此时,A 和 B 都会向对方发送”接收方未就绪”(RNR)的确认消息(TAACK)。如果 A 发给 B 的 TAACK 被 A 发往 B 的 Send
数据流阻塞,同时 B 发给 A 的 TAACK 也被 B 发往 A 的 Send
数据流阻塞,那么双方都无法处理对方的 RNR 消息,也无法释放自己的接收队列,从而形成死锁。
为了避免这类消息通信死锁,UB 提供了三种基础机制:
- 传输层和事务层分离:这是一个关键的解耦设计。即使上层的消息事务处理因为功能性资源不足(例如应用逻辑繁忙)而被阻塞,底层的传输协议层依然可以独立运行,不会被阻塞。这避免了单点的应用层拥塞,扩散成大范围的、电路级别的网络反压。
- 事务层应答返回资源状态:当事务层因为资源不足无法处理请求时,它会通过应答消息明确地将资源状态(例如”繁忙”)返回给发起方。发起方收到这种应答后,可以自行决定是重试,还是采取其他策略,从而避免在网络链路上产生死锁等待。
- 超时机制:为消息通信设置超时。如果一个操作长时间没有完成,系统会判定其失败,并释放其占用的资源。这是一种最终的保障机制,确保即使发生死锁,系统也能在超时后自行恢复,保持链路的畅通。
URPC:为异构硬件设计的远程过程调用
行文至此,我们已经为 Unified Bus 构建了一个坚实的基座。URMA 提供了强大的、对等的远程内存访问能力,无论是与 CPU 指令集深度融合的 Load/Store
,还是更为灵活的异步 Read/Write
,都为硬件单元之间的数据交换铺平了道路。然而,这套内存语义的抽象,对于上层应用的开发者来说,仍然过于底层。它好比是为开发者提供了一套强大的汇编指令,却缺少一个高级语言的编译器。
应用开发者思考业务逻辑时,最自然的模型是”函数调用”,而不是”内存读写”。我们需要一种方式,将 UB 强大的内存级通信能力,封装成一种更高级、更易于理解和使用的抽象。这就是 URPC (Unified Remote Procedure Call) 诞生的初衷。
RPC 的文艺复兴:从软件服务到硬件功能
传统的 RPC 框架,如 gRPC、Thrift,早已是分布式软件开发的基石。它们的核心思想,是将跨机器的函数调用,模拟得像本地调用一样简单。然而,这些框架的设计哲学,是深深根植于一个”CPU 中心”的世界观的:
- 通信主体是软件进程:RPC 的发起方和执行方,都是运行在 CPU 上的软件服务。
- 数据通路依赖操作系统:所有数据收发都必须经过内核的 TCP/IP 协议栈,带来不可避免的拷贝和上下文切换开销。
- 参数传递是”值传递”的天下:由于没有共享内存,所有参数,无论多大,都必须被序列化、拷贝、通过网络传输,再反序列化。
在 UB 所描绘的异构、对等计算的图景中,这套传统模型显得力不从心。我们需要调用的,不再仅仅是另一台服务器上的一个软件函数,而可能是:
- CPU 上的应用,需要调用 NPU 上的一个硬件加速算子。
- GPU 上的一个 Kernel,需要调用远端内存控制器上的一个数据整理函数。
- 一个数据处理单元(DPU),需要调用集群中另一台 DPU 上的一个网络功能。
URPC 的核心使命,就是为这种异构硬件之间的、高性能、细粒度的直接通信,提供一个标准的”函数调用”抽象。它不再仅仅是软件之间的通信协议,更是异构硬件单元之间协同工作的”功能层”协议。
按引用传递:当指针可以跨越机器
URPC 的设计中最具革命性的一点,是它原生、高效地支持按引用传递(Pass-by-Reference)。这在传统的 RPC 世界中是不可想象的,但在 UB 的体系下,这却是顺理成章的。
我们之所以能够做到这一点,正是因为 URPC 的”引用”,并非一个普通的、只在本地进程有意义的虚拟地址。它是一个全局有效的 UB 地址。当一个 UBPU(例如 CPU)向另一个 UBPU(例如 GPU)发起一个 URPC 调用,并传递一个数据结构的引用时,它传递的是一个通行无阻的”钥匙”。远端的 GPU 硬件拿到这个地址后,无需 CPU 的干预,可以直接通过底层的 URMA Load/Store
硬件指令,跨越网络,直接访问发起方内存中的数据。
这种能力的价值是巨大的。想象一下,一个 AI 训练任务需要调用一个函数,来处理一个上百 GB 的模型权重或数据集,而这个函数只需要读取或修改其中的一小部分数据。
- 在传统 RPC 中,这意味着一次上百 GB 的数据拷贝。如果希望节约拷贝数量,就需要调用方把被调用方需要访问的数据 “抠” 出来,导致调用方和被调用方架构上的耦合。
- 在 URPC 中,我们只需传递一个指向整个数据结构的引用。被调用的函数(Callee)可以按需、精准地只抓取它所需要的那部分数据,从而避免了海量、无谓的数据搬运。
更有趣的是,这种设计为更精细的性能优化打开了大门。如果上层的编程语言或 API 有能力区分只读引用 (&T
) 和 可写引用 (mut &T
) ,URPC 就可以将这个信息一直传递到最底层的 UB 硬件。面对一个只读引用,硬件知道这份数据不会被修改,于是可以大胆地启用更激进的缓存策略,而无需担心复杂的缓存一致性维护开销。
按值传递:当拷贝不可避免
当然,按引用传递并非万能灵药。在很多场景下,我们依然需要传统的按值传递(Pass-by-Value)。例如:
- 异构数据结构:当调用发生在两种不同的编程语言或硬件架构之间时,它们对数据结构的内存布局定义可能完全不同。此时,必须进行格式转换和数据拷贝。
- 小数据量参数:对于一些很小的参数(如配置项、标量值),为其建立远程内存映射、再通过
Load/Store
读取的开销,可能比直接将数据打包在请求中一次性发过去还要大。
URPC 充分认识到了这一点,因此它也为按值传递的场景提供了高效的支持。但这里的”高效”,与传统 RPC 框架中的 protobuf
或 JSON
序列化有本质区别。URPC 的序列化/反序列化(SerDes)机制,是为硬件设计的。其目标是拥有极简的格式和最低的计算复杂度,使得这个过程可以被最大程度地下沉到硬件中去完成,从而将宝贵的 CPU 资源从繁琐的数据打包/解包工作中解放出来。
UB 超节点:大模型软硬件生态系统的世界第三极
行文至此,我们已经深入探讨了 Unified Bus 从设计哲学到关键技术实现的漫漫长路。然而,任何一项技术,无论其设计多么精巧,最终都要在一个具体的、有形的系统中得到验证,才能彰显其真正的价值。这个系统,就是基于 UB 架构的超节点(SuperPoD)。它不仅仅是一款产品,更是我们最初所有思考、所有论证、所有坚持的最终答案。
回顾 UB 项目的源起,我们最初的梦想,是打破总线与网络之间泾渭分明的壁垒,创造一种兼具总线级性能与网络级规模的全新互联范式。我们坚信,未来的计算模式,必然要求我们将整个数据中心的资源——算力、内存、存储——视为一个统一的整体,构建出一台逻辑上的”巨型计算机”。
在当时,这个想法被许多人视为天方夜谭。主流观点认为,8 卡服务器的节点内互联已经足够,跨节点的通信远不需要如此极致的性能。然而,当 Scaling Law 成为 AI 领域颠扑不破的”物理定律”时,人们终于意识到,单节点的算力早已触及天花板,成千上万个处理器必须以前所未有的效率协同工作,而连接它们的互联技术便成为了决定整个系统成败的胜负手。
正是在这样的时代背景下,UB 超节点应运而生。它不再是纸上的协议,而是一个在生产场景中接受大量检验的大规模计算系统。UB 超节点的架构师们通过一系列关键技术特征,将我们曾经的设想变为现实:
- 大规模组网:这是超节点最核心的能力。为了支撑超大规模模型训练,超节点必须突破传统网络的扩展瓶颈。我们为此设计了 UB-Mesh 组网架构,其核心是一种被称为 nD-FullMesh 的拓扑,它充分利用 AI 训练负载的流量局部性,通过高密度的短程直连,以极低的成本和延迟连接海量节点。在此基础上,通过 2D-FullMesh 与 Clos 的混合拓扑,超节点可以实现 8192 卡规模下超过 90% 的线性加速比,并为未来扩展至百万卡集群的 UBoE(UB over Ethernet) 方案预留了接口。
- 总线级互联:在巨大的规模之上,超节点依然保持了总线级的极致性能。UB 提供了百纳秒级的同步内存语义访问(用于对延迟极度敏感的 Load/Store 指令)和 2~5 微秒的异步内存语义访问(用于大块数据的 Read/Write),节点间带宽可达 TB/s 级。
- 全量池化与平等协同:在 UB 的连接下,整个超节点的所有资源——无论是 NPU、CPU 的算力,还是 DRAM 内存、SSD 存储(SSU)——都被汇聚成一个统一的资源池。更重要的是,这些资源是平等的,任何一个 NPU 都可以直接访问另一个节点的内存,绕过 CPU,实现去中心化的协同工作。
- 协议归一:支撑这一切的,是 UB 统一的协议栈和编程模型。它消除了传统架构中 PCIe、Ethernet、InfiniBand 等多种协议并存带来的转换开销和管理复杂性,让上层应用可以用一套统一的语义,高效地驾驭整个集群的异构资源。
- 高可用性:一个拥有数万光模块的系统,其可靠性是巨大的挑战。UB 通过分层的可靠性机制来应对:在链路层,有 LLR(Link Layer Retry)来处理瞬时误码;在物理层,支持链路降级(Lane Degradation)和 2+2 光模块备份,实现故障的业务无感恢复;在传输层,端到端的重传机制是最后的保障。这些机制共同确保了在 8192 卡的超大规模下,光互联的平均无故障时间(MTBF)超过 6000 小时。
放眼全球,能够支撑起超大规模 AI 模型训练和推理的硬件生态屈指可数,因为这需要芯片、网络、乃至操作系统的深度协同,是只有具备软硬件全栈能力的公司才能完成的艰巨任务。此前,这个舞台上只有两位主角:
- NVIDIA:以其 GPU 为核心,通过收购 Mellanox 补齐了 InfiniBand 网卡和交换机的版图,又推出 Grace CPU 并试图收购 ARM,不断巩固其”GPU + DPU + CPU”的”三芯”战略,最终构建出 DGX SuperPOD 这一强大的生态系统。
- Google:作为另一位巨头,其 TPU 硬件与内部的软件生态深度绑定,构成了另一个封闭而高效的王国。世界上推理速度(每秒输出 token 数量)最快的 SOTA 模型,很多是运行在 Google TPU 之上。
它们通过各自的方式,解决了万卡规模的扩展性和效率问题,从而定义了这个时代的算力格局。
作为一名从最初就参与其中的普通工程师,回望这段历程,感慨万千。我们最初的坚持,源于一种不同的世界观——我们相信未来的计算,必然是构建在一台逻辑统一的’数据中心计算机’之上。最初,只有十来位架构师围在白板前研讨原型,那时的我们更像是在布道,努力说服大家相信一个尚未到来的未来。
真正的转折点发生在 2020 年 GPT-3 发布之后。它以无可辩驳的性能展示了 Scaling Law 的威力,也让我们所坚持的愿景得到了公司内广泛的认可。自那以后,UB 获得了更大的投入,曾经十余人的小团队,迅速成长为上千人直接参与的庞大项目。
今天,随着 UB 超节点的规模化量产、Unified Bus 协议的正式发布,我们当年在白板上画下的那些通信原语和架构,终于真正落地,并向更广阔的生态开放。UB 的诞生,为昇腾生态补上了最关键的一块拼图,标志着继 NVIDIA 的 GPU 和 Google 的 TPU 之后,全球出现了第三个能够支撑起顶级大模型训练和推理的完整软硬件生态系统。
这个曾经被视为天方夜谭的愿景,已经随着 UB 超节点的落地而成为新的’实在’。这或许就是技术演进的魅力,如同科学革命一样,它始于少数人对世界’应该是什么样’的重新想象,并最终成为整个行业对’世界是什么样’的全新共识。