CXL是基于PCIe 5.0的物理层发展而来的,CXL支持三种类型的协议,分别为CXL.io、CXL.cache和CXL.memory。本文主要介绍CXL的事务层,包括CXL.io事务层、CXL.cache事务层和CXL.mem事务层,如下图所示。
1. CXL.io事务层
CXL.io为IO设备提供了非一致性的load/store接口,负责设备的发现、枚举、配置、错误报告、中断等。事务类型、事务包格式、credit流控、虚拟通道管理、事务排序的规则都遵循PCIe协议。
1.1 CXL.io Endpoint
CXL设备需要支持CXL 1.1和CXL 2.0模式。CXL的协议协商决定了操作模式。当链路配置为CXL 1.1模式时,CXL.io?endpoint必须作为PCIe RCiEP暴露给软件;当配置为CXL 2.0模式时,必须作为PCIe Endpoint暴露给软件。
CXL设备的function如果参与了CXL.cache或CXL.mem协议,则不能生成INTx消息。
1.2 CXL电源管理VDM格式
CXL电源管理message使用PCIe的VDM(Vendor Defined Message) Type 0,带有4DW的负载数据,包括PMREQ,PMRSP和PMGO消息。
CXL电源管理VDM的格式如下:
如果CXL组件收到带“poison”的电源管理VDM,则应该丢弃这个消息。由于接收方在收到此类VDM后能够继续正常工作,所以应将此事件视为非致命(non-fatal)错误。
如果接收方的电源管理单元(PMU)不知道电源管理VDM数据的含义,应默默丢弃该消息,并且不发出不可纠正(uncorrectable)的错误。
1.3 CXL错误消息VDM格式
CXL错误消息使用PCIe的VDM Type 0,没有负载数据。目前只有一种错误消息类型:Memory Error Firmware Notification (MEFN),其格式如下。
错误消息总是由device发起,使用“Routed to Root Complex”的路由。
1.4 CXL所需的可选PCIe特性
1.5 错误传播
设备检测到的CXL.cache和CXL.mem错误通过CXL.io传播到上游端口。这些错误在PCIe AER寄存器中记录为可纠正和不可纠正的内部错误。
1.6 ATS上的内存类型指示
对某些内存区域的请求只能由CXL.io发出,而不能由CXL.cache发出。由主机决定这些内存区域是什么。例如,在x86系统上,主机可以限制只能通过CXL.io访问Uncacheable类型的内存。主机通过ATS Completion向设备指示这些区域。
来自CXL device的ATS请求必须设置CXL Src bit,0表示不支持内存类型的指示;1表示支持内存类型的指示,所有的CXL设备function必须设置该bit。
来自主机的ATS translation completion携带CXL.io bit,0表示这个page可以被CXL所有协议访问,1表示这个page只能由CXL.io访问。
1.7 可延迟(Deferrable)写
CXL规范中定义的可延迟写入仅适用于CXL 1.1模式。在CXL 2.0模式下工作时,可参考PCIe规范了解此功能。可延迟写入允许多个软件实体向CXL设备提交工作,而无需显式锁或软件同步。可延迟写是non-posted内存写。可延迟写操作的完成允许设备指示该命令是否已成功接受,或者是否需要延迟执行。
CXL.io的Deferrable写是通过NPMemWr32/64事务发送的。
2. CXL.cache事务层
2.1 概述
CXL.cache协议将设备和主机的交互定义为多个请求,每个请求至少有一条响应,有时还有数据传输。CXL.cache接口在每个方向上有三个通道:Request、Response和Data。独立的通道允许不同类型的消息使用专用的信号线,实现解耦并提高每条线的吞吐率。D2H表示设备到主机,H2D表示主机到设备。
D2H请求:将请求从设备发到主机。请求通常是访问内存的。每个请求将收到0、1或2个响应,和最多一个64字节的cacheline。D2H请求可以被反压。
D2H响应:将设备的响应发给主机。设备对snoop的响应返回该cacheline在设备缓存中的状态,并可能表示数据正返回到主机提供的数据buffer。它们可能会因为链路层credit被暂时阻塞。
D2H数据:将数据和Byte enable从设备传到主机。数据传输可以是隐式(由于snoop)或显式写回(由于cache eviction)。在所有情况下,都将传输完整的64-byte缓存行。D2H数据传输必须进行,否则可能出现死锁。它们可能会因链路层credit暂时被阻塞,但不能要求完成其他事务来释放credit。
H2D请求:请求从主机到设备。这些请求都是维持一致性的snoop。可能会返回数据。H2D请求可以被反压。
H2D响应:携带ordering消息并pull写数据。每个响应都携带来自原始请求的请求标识符,以指示响应需要路由到哪里。
H2D数据:为设备的读请求提供数据。所有情况下,都传输完整的64字节缓存行。
2.2 CXL.cache通道介绍
通道ordering:通常,所有CXL.cache通道都必须彼此独立工作。然而,也有特例需要维护通道之间的顺序。主机需要等待设备观察到H2D响应上发送的全局排序(Global Ordering,GO)消息,然后再发送相同地址的snoop。为了限制记录GO消息所需的buffer大小,主机保证通过CXL.cache发送的GO消息后,在一定周期内不能再发送snoop。
通道credit:每个通道必须使用credit来发送消息。在操作过程中,每当接收方处理完消息后,它都会返回一个credit。如果没有可用的credit,发送方必须等接收方返回信用。下表描述了哪些通道必须被排空才能继续,哪些通道可以无限期阻塞。
2.3 CXL.cache信号
CXL.cache每个通道的字段定义如下。
D2H请求:
D2H响应:
D2H数据:
H2D请求、响应、数据和D2H类似,具体可以参考CXL协议。
2.4 CXL.cache事务
D2H请求:
D2H请求有四种语义:CXL.cache Read、CXL.cache Read0、CXL.cache Read0/Write、CXL.cache Write。
CXL.cache Read请求需要有D2H请求credit,在D2H CXL.cache请求通道上发送,需要0或1条响应(GO)消息和64字节的数据信息。响应和数据都指向D2H请求的CQID字段中的tracker entry。在收到来自主机的所有消息之前,设备entry必须保持active状态。为确保事务传输,设备必须有预留的数据buffer,以便在发送请求后能立即接收所有数据。但是,由于先前收到的数据没有排出,设备可能暂时无法接收主机的数据。接收到来自主机的响应和数据,就可以认为事务已完成,并且entry从设备中释放。
下图是CXL.cache Read的过程。响应(GO)消息可以在数据消息之前、之后或之间接收。
CXL.cache Read0请求需要有D2H请求credit。会收到响应,但不会收到数据。响应指向请求的CQID指示的entry。一旦收到GO响应,就认为它们已完成,并且entry从设备中释放。主机不能给这些事务发送数据。
CXL.cache Write请求需要有D2H请求credit。主机需要发送两条单独的或一条合并的GO-I和WritePull消息。GO消息不能在WritePull之前到达设备,但它可以在组合消息中到达。如果是posted类型的写,那么可以使用合并的GO-I和WritePull消息。如果是non-posted写,host需要先发GO-I消息,再发WritePull消息。
下图是CXL.cache Write事务的过程,WritePull消息触发数据消息。
下图为WrInv事务流程,WritePull和GO是单独的消息。(例如:强序的不可缓存的写)
下图是主机FastGO加上ExtCmp对弱序写请求的响应。
CXL.cache Read0-Write需要D2H请求信用。主机需要发送一条合并的GO-I和WritePull消息。WritePull会触发设备发出数据给主机。一旦设备收到GO-I消息并发送了所有数据,则认为事务已完成。此时,可以从设备中释放entry。主机在收到所有64字节的数据并发送GO-I响应后,会认为Read0-Write结束。
D2H请求的opcode如下:
- RdCurr(Read Current):来自设备的完整缓存行读请求,以获取最新数据,但不会更改缓存(包括主机)中的现有状态。主机不需要记录发RdCurr请求的设备中的缓存行。RdCurr获取数据,但不会GO。设备接收invalid状态的cacheline,这意味着它只能使用该行,而不能缓存。
- RdOwn:来自设备的完整缓存行读请求,用于缓存处于可写状态的行。通常,RdOwn请求接收处于Exclusive(GO-E)或Modified(GO-M)状态的行。处于Modified状态的缓存行,必须将其写回主机。在错误情况下,RdOwn请求可能会收到Invalid(GO-I)或Error(GO-Err)状态的行。两者都将返回全是1的数据。设备负责处理错误。
- RdShared:设备发起的读完整缓存行的请求,用于缓存Shared状态的行。通常,RdShared请求接收处于共享(GO-S)状态的行。在错误情况下,RdShared请求可能会收到Invalid(GO-I)或Error(GO-Err)状态的行。两者都将返回全是1的数据。设备负责处理错误。
- RdAny:设备发起的读取完整缓存行的请求,用于缓存处于任何状态的行。在错误情况下,RdAny请求可能会收到Invalid(GO-I)或Error(GO-Err)状态的行。两者都将返回全是1的数据。设备负责处理错误。
- RdOwnNoData:设备向指定的缓存行地址发起该请求,目的是拥有独占的权限(GO-E)。在错误情况下,可能会收到Error(GO-Err)状态的行。设备负责处理错误。
- ItoMWr:设备对指定的缓存行地址发起独占权限请求,并以原子方式将缓存行写回主机。由于设备会将整个缓存行的数据都修改掉,所以不需要主机将该缓存行数据发给设备。当主机给了独占权限后,设备会回复响应GO-I-WritePull。设备不需要再保留写回的数据。如果错误发生,设备回复GO-Err-WritePull,主机丢弃设备写回的数据。设备处理该错误。
- MemWr:与ItoMWr类似,但是写回数据的位置可能不同。只有当命令命中缓存时,数据才会写入;如果未命中,数据将直接写入内存。一旦请求被授予ownership,典型的响应就是GO-I-WritePull。设备不保留数据副本。如果错误发生,设备回复GO-Err-WritePull,主机丢弃设备写回的数据。设备处理该错误。
- ClFlush:设备发起请求,无效掉指定地址的缓存行。主机返回的典型响应是GO-I。CleanEvict:设备发送请求给主机,evict一条64字节的Exclusive缓存行。通常会收到GO-WritePull或GO-WritePullDrop响应。不论设备收到哪种响应,都必须放弃对该缓存行的监听权限。如果收到GO-WritePull,那么设备将数据发送给主机,如果是GO-WritePullDrop,那么设备将丢弃该数据。
- DirtyEvict:设备发送请求给主机,evict一条64-byte的Modified缓存行。通常应收到GO-WritePull响应,这意味着设备对该缓存行失去监听权限,并需要将数据发送出去。一旦设备发起这个请求,并在设备收到GO-WritePull响应前,访问的缓存地址已经被监听了,这时设备需要设置数据的Bogus字段,用来指示这个数据不是最新的。当有错误出现,主机回复GO-Err-WritePull,设备写回的数据会被主机丢弃。
- CleanEvictNoData:设备发起请求给主机,以便在设备中删除一条干净的缓存行。此请求的唯一目的是更新主机中的snoop filter,不会交换数据。只有在主机挂载内存的地址范围,才需要CleanEvictNoData。对于设备挂载内存的地址范围,等效操作可以在设备内部完成,无需向主机发送事务。
- WOWrInv(weakly orderd write invalid):弱序写无效缓存行(0-63 byte)请求,可以设置byte enable。通常收到一个FastGO-WritePull,后跟一个ExtCmp。收到FastGO-WritePull后,设备将数据发送到主机。对于主机挂载内存,一旦内存中的写入完成,主机就会发送ExtCmp。
- WOWrInvF:与WOWrInv类似,不同的是它是一条完整的缓存行(64byte)。WrInv:0-64字节的写无效请求。通常会收到一个WritePull,然后是GO。获得WritePull后,设备将数据发送到主机。一旦内存中的写入完成(主机挂载内存或设备挂载内存),主机将发送GO。
- CacheFlushed:设备通过此请求通知主机其缓存已刷新,并且不再包含Shared、Exclusive或Modified状态的任何缓存行。主机可以用此信息清除其snoop filter,阻止对设备的监听并返回GO。一旦设备收到GO,在设备发送下一个可缓存D2H请求之前,保证不会从主机接收任何监听。
D2H响应:
- RspIHitI:设备对主机的snoop请求的响应,表示设备没有此缓存行。如果设备返回RspIHitI,主机可以认为该缓存行已从设备中清除。RspVHitV:设备对主机的snoop请求的响应,表示设备有此缓存行,但是状态没有改变。
- RspIHitSE:设备对主机的snoop请求的响应,表示设备里的缓存行是clean的,而且现在是无效的。如果设备返回RspIHitSE,主机可以认为该缓存行已经从设备中清掉。
- RspSHitSE:设备对主机的snoop请求的响应,表示设备里的缓存行是clean的,而且现在降级为Shared。RspSFwdM:H2D的snoop请求命中的缓存行是Modified状态,之后变为了Shared状态。设备可以将状态降级为Invalid。
- Fwd(forward)表示设备会通过D2H CXL.cache数据通道发送64-byte数据给主机。
- RspIFwdM:H2D的snoop请求命中的缓存行是Modified状态,之后变为了Invalid状态。主机可以认为设备不再有该缓存行。设备会发送64-byte数据给主机。
- RspVFwdV:缓存行的状态没有发生变化。设备仅把当前的数据发送给主机。
H2D请求:
下图是H2D snoop请求的过程。主机可以按照与数据的任何相对顺序接收响应消息。对于snoop数据传输,byte enable字段始终为全1。
- SnpData:主机发起的snoop请求,针对Shared或者Exclusive状态的缓存行。设备收到该请求后,必须将缓存行invalidate或者降级为Shared状态。如果设备有dirty数据,必须写回给主机。
- SnpInv:主机发起的snoop请求,主机想拥有缓存行的所有权,状态为Exclusive。设备收到该请求后,必须将缓存行invalidate。如果设备有dirty数据,必须写回给主机。SnpCur:该snoop请求是为了获得最新的缓存行,但不改变缓存现有状态。它只代表RdCurr请求发送。如果设备中有Modified的缓存数据,必须写回给主机。主机和设备的缓存行状态不需要改变,主机不更新缓存。
H2D响应:
- WritePull:主机告诉设备,把写数据发给主机,但是不改变缓存行的状态。用于WrInv请求,数据需要先于GO-I消息发出,因为GO-I消息发出,意味着I/O的写操作已经完成
- 。GO(Global Observation):表示读请求是coherent,写请求是coherent且consistent。系统设备已观察到该事务,RspType字段中编码的MESI状态表示数据应置于请求者缓存的哪个状态。
- GO_WritePull:GO+WritePull消息。没有缓存状态需要传给设备。GO+WritePull消息用于posted写。
- ExtCmp:系统观察到了有Fast_GO数据。访问内存会得到最新的数据。
- GO_WritePull_Drop:与Go_WritePull相同,只是设备不应向主机发送数据。当主机确定不需要数据时,可以发送此响应来代替GO_WritePull。由于始终需要传输byte enable,因此不会为partial写发送此响应。
- Fast_GO:与GO消息类似,但只指示请求是locally observed。(GO消息是全系统observed)。当事务是fully observable时,会发出ExtCmp消息。如果设备不支持Fast_GO特性,会忽略这条消息,一直等待ExtCmp。
- Fast_GO_WritePull:与GO_WritePull类似,但只指示请求是locally observed。当事务是fully observable时,会发出ExtCmp消息。如果设备不支持Fast_GO特性,则忽略这条消息,等待ExtCmp消息。主机不需要传缓存状态给设备。
- GO_ERR_WritePull:与GO_WritePull类似,但表示事务出现错误,需要设备去处理。对于WritePull,设备必须把数据发给主机,主机会将数据丢弃。主机不需要传缓存状态给设备。
2.5 Cacheability介绍和请求限制
本节的描述和限制适用于所有的设备。
GO-M响应
主机的GO-M响应表明设备被授予modified data的唯一副本。设备必须缓存此数据,并在完成后将其写回。
Device/Host Snoop-GO-Data假设
当主机向设备返回GO响应时,期望到达接收GO的请求的相同地址的监听将看到该GO的结果。例如,如果主机给RdOwn请求发送GO-E响应,然后紧接着向同地址发送监听,接下来会期望设备将缓存行转换为M状态,并返回RspIFwdM响应给主机。为实现这一目的,CXL.cache链路层确保设备在不同的slot接收这两条消息,以使顺序完全明确。
当主机向设备发送监听请求时,要求在主机收到监听响应并收到所有隐式写回(implicit writeback,IWB)数据之前,不会向设备中含有该地址的任何请求发送GO响应。当主机向设备返回数据,并且该请求的GO尚未发送到设备时,主机可能在发送GO消息之前不会向该地址发送监听请求。
从根本上讲,与读请求相关的GO也适用于该请求返回的数据。为读请求发送数据意味着数据有效,即使GO尚未到达,设备也可以使用它。
Device/Host Snoop/WritePull假设
在主机收到来自64字节地址的所有WritePull数据之前,主机不可以对该地址启动监听。相反,在主机收到对挂起的写入地址监听的响应之前,主机不可以启动WritePull。
CXL.cache上Evict的监听响应和数据传输
如果在CXL.cache D2H请求通道上发出了设备Evict事务,但尚未从主机收到WritePull,并且监听命中WB,则设备必须记录此监听命中。当设备开始处理WritePull时,设备必须在发送到主机的所有D2H数据中设置Bogus字段。目的是向主机传达请求数据已作为IWB数据发送,因此evict的数据可能已过时。
对相同地址的多个监听:只允许主机对指定设备的指定缓存行地址有一个outstanding监听。主机必须等收到监听响应和IWB数据(如果有)后,才能将下一个snoop发送到该地址。
对同一缓存行的多个读请求:只有在以下特定情况下,无论处理请求的顺序如何,主机跟踪状态都是一致的,才允许向同一个缓存行发送多个读请求。主机可以自由地对请求重排序,因此设备负责对请求排序。对于主机内存,允许多个RdCurr/CLFlush。对于这些命令,设备以I-state结束,因此主机跟踪设备缓存时不可能存在不一致的状态。对于Type 2设备,除了RdCurr和/CLFlush之外,还允许多个RdOwnNoData用于设备连接的内存。这种情况是允许的,因为在设备连接内存时,主机不会跟踪设备的缓存,所以在主机中重排序将不会在设备和主机之间创建模糊的状态。
对同一缓存行的多个evict:不允许对同一缓存行发起多次evict。
对同一缓存行的多个写请求:允许在CXL.cache上对同一缓存行执行多个WrInv/WOWrInv/ItoMWr/MemWr。主机或switch可以对请求进行重新排序,并且设备按照重排序的顺序接收H2D响应。但是,通常不建议设备对同一缓存行发出多个写请求。
Normal?GO:只有在主机保证请求将拥有缓存行的所有权之后,才会发送GO响应。
Relaxed Global Observation(FastGO):FastGO只允许用于不需要严格排序的请求。
evict到设备挂载的内存:在CXL.cache中,不允许evict到与设备相连的内存中。设备只允许向设备连接的内存发出WrInv、WOWrInv*。
CXL.cache上的内存类型:要在CXL.cache上发出请求,设备需要通过CXL.io上的ATS请求从主机获取主机物理地址。
3. CXL.mem
3.1 介绍
CXL内存协议叫作CXL.mem。CXL.mem定义了CPU和内存之间的传输接口。该协议可用于多个不同的内存连接选项:包括内存控制器位于主机CPU时,或位于加速器设备时,或位于内存缓冲芯片时。
CPU中的一致性引擎使用CXL.mem请求和响应与内存相连。在此配置中,CPU一致性引擎被视为CXL.mem的Master(主设备),内存设备视为CXL.mem的Subordinate(从设备)。Master负责发起读写请求,Subordinate负责响应Master的读写请求(例如data、completion)。
当从设备是加速器时,CXL.mem协议认为设备内也有一个一致性引擎(Device Coherency Engine,简称DCOH)。
从Master到Subordinate的CXL.mem事务称作M2S;从Subordinate到Master的事务称作S2M。
M2S事务有两种消息类型:
- 无数据的请求(Req)有数据的请求(RwD)
相应的,S2M事务也有两种类型:
- 无数据的响应(NDR)有数据的响应(DRS)
3.2 内存的QoS遥测
QoS全称为Quality of Service,即服务质量。内存的QoS遥测是内存设备的一种机制,用于在CXL.mem请求的响应中指示当前负载级别(DevLoad)。这使主机能根据负载级别来衡量对部分设备、单个设备或设备组的CXL.mem请求的速率,从而优化内存设备的性能,同时限制结构拥塞。详细内容可参考CXL协议。
3.3 M2S Request(Req)
Req是无数据的请求,包括读操作、invalid操作,其字段定义如下:
3.4?M2S Request with Data (RwD)
RwD是有数据的请求,字段定义如下:
3.5?S2M No Data Response(NDR)
NDR消息包含从Subordinate到Master的完成和指示,不带数据。
3.6?S2M Data Response(DRS)
DRS消息包含从Subordinate到Master的内存读数据。
3.7?Forward Progress and Ordering Rules
- 在多跳互联网络中,Req和RwD的每一跳都需要credit。允许被反压。如果请求和MemRdFwd/MemWrFwd访问相同的缓存行地址,则M2S Req通道中的CXL.mem请求不能发送MemRdFwd或MemWrFwd。NDR和DRS消息需要在source位置预先分配。在CXL.mem上,只有在写完成后,写入数据才能保证对之后的访问可见。CXL.mem请求需要在设备上进行转发,而不依赖于任何设备发起的请求。这包括CXL.io或CXL.cache设备的任何请求。M2S和S2M的缓存行数据传输不能与其它缓存行交织。数据必须以自然的块顺序传输,也就是64B传输必须先完成低32B传输。
参考:CXL Specification 2.0/3.1