延续队列
本页所有地址均适用于来自
libtpu-0.0.40-cp314wheel 的libtpu.so(构建libtpu_lts_20260413_b_RC00,build-id md589edbbe81c5b328a958fe628a9f2207d)。该映像未剥离符号;反修饰后的 C++ 符号名按原文逐字引用。.textVMA 等于文件偏移(.text基址0xe63c000);.data.rel.ro带有0x200000的 VMA→文件差值;protodesc_cold(嵌入的FileDescriptorProto文本)位于 VMA==offset0xbe8af30。其他版本会有所不同。
摘要
延续队列是 TPU 程序在不停止芯片的情况下结束的方式。普通 LLO 程序以 scalar-halt(LloOpcode 0x25)终止,这会阻塞 sequencer,直到主机提交下一个程序。当 TensorCore 延续队列特性启用(Target+0x628 的 bit-0)时,尾部 halt 会被抑制,并由一个单独编译的 continuator program 替代;它推进描述符环、DMA 一个逐程序控制记录、触发主机中断,并直接尾调用到下一个程序的入口点。链路按 program→continuator→program→…→scalar-halt 运行;只有没有已入队后继的程序才真正 halt。这是 libtpu 对启动延迟的回答:在 Megacore 运行中,设备在背靠背程序之间永不空闲。
三个协作层承载该机制。(1) 配置 proto — TpuChipConfigProto.ContinuationQueue,按 core 注册到 Target 中的 xla::jellyfish::Target::ContinuationQueueConfig 区域(Target+0x580 + core*0x38)— 声明四个生产者 sync-flag 字偏移、completion/error sflags、主机中断号,以及一个由 {shared_memory_region, consumer_sync_flag} 组成的 per_core 向量。(2) 异步描述符记录是一个扁平的 int32 SMEM 控制块(位于 Target+0x808..+0x8b8 的逐程序块),由 ReservedSmemFiller::FillCoreBuffer 在主机侧构建,每个字段放在 TpuMemoryReservationType 预留的字槽中,然后 DMA 到共享内存环中。(3) 运行时环 — tpu::ContinuationQueue — 是带锁保护的主机侧生产者/消费者,并有一个异步工作线程:Enqueue 提交 Request,WorkerLoop 弹出它并派发延续(延续会用 WriteMemory DMA 该记录),Completed 是设备中断处理器。
本页负责说明配置 proto、异步描述符记录、运行时 drain 协议,以及 scalar-halt 抑制映射。描述符的 enums — (mem_id, core_id) 内存空间编码、src/dst opcode 枚举、OCI 描述符字段布局 — 属于 芯片内 DMA 描述符,此处不重复;将记录实际移动到设备 SMEM 的主机↔设备传输见 UHI 主机接口。已经理解带生产者索引、槽位数量和 completion 标志的有界环形缓冲区的读者会立刻认出其形状;本页记录的两个意外之处是:(1) “descriptor” 不是打包 struct,而是一个扁平 int32 数组,通过 reservation-type 字槽寻址,也就是设备经由 Target 访问器读回的同一块内存;(2) 程序终止是一个拆分程序模型 — halt 和 continuation 不是二选一,而是由 Target+0x628 bit 在两半之间切换。
对于重新实现,契约是:
- 配置 proto — 10 字段的
ContinuationQueue消息(字段 #2 是编号空洞)、其PerCore子消息、SharedMemoryRegion,以及asic_sw.deepsea.SyncFlag消费者标志句柄 — 包含字段号、类型和 C++ 成员偏移。 - 异步描述符记录 — 扁平
int32SMEM 控制块,说明哪个TpuMemoryReservationType槽持有哪个字段、值来源、poison 填充值,以及记录字节大小 =reserved_words*4向上取整到max(0x200, count)的倍数。 - 运行时 drain —
tpu::ContinuationQueue对象布局、环窗口算术,以及Enqueue→WorkerLoop→WriteMemory→Completed路径;再加上设备侧生产者环推进EmitContinuationTailcall。 - scalar-halt 映射 — 五个
ShaltInternal程序末尾发射点、CompileInternal中Target+0x628bit-0 的抑制、更丰富的LowerHloModuleImpl逐 sequencer gate,以及 continuator 自己的收尾 halt。
| 配置 proto | TpuChipConfigProto.ContinuationQueue(DescriptorProto @ 0xc18e12d,len 649) |
| 运行时配置区域 | xla::jellyfish::Target::ContinuationQueueConfig @ Target+0x580 + core*0x38(stride 0x38) |
| 设备生产者 | xla::jellyfish::continuations::EmitContinuationTailcall(LloRegionBuilder, long) @ 0x12718ca0 |
| Continuator 编译 | deepsea_compiler_backend::CompileContinuationTailCall @ 0x10a28b00 |
| 记录构建器 | tpu::ReservedSmemFiller::FillCoreBuffer @ 0x1d4c1d60;大小 @ GetRequiredDescriptorBytes 0x1d4c2a20 |
| 运行时环 | tpu::ContinuationQueue ctor @ 0x1d160ae0;Enqueue 0x1d161160;WorkerLoop 0x1d162ba0;Completed 0x1d1616a0 |
| 入队驱动器 | tpu::RealProgramContinuator::EnqueueProgram @ 0x1d153ca0 |
| Halt opcode | ShaltInternal @ 0x1d520d20 = CreateNullaryOp(0x25)("scalar-halt")+ AppendInstruction |
| 启用 bit | Target+0x628 bit-0(存在 TC 延续队列区域) |
| 证据等级 | 重新实现级 / 已按字节对 IDA 反编译 + 切出的 FileDescriptorProto + TcParseTable 偏移数组确认 |
1. ContinuationQueueConfig Proto
目的
TpuChipConfigProto.ContinuationQueue 是静态配置记录,用于告诉主机运行时和设备生产者队列位于何处。每个 core 注册一条消息(TpuChipCommonImpl::RegisterContinuationQueueConfigs(AnySpan<const TpuChipConfig::ContinuationQueue>) @ 0xe72b340),并复制到 Target 中作为位于 Target+0x580 + core*0x38 的 Target::ContinuationQueueConfig 区域。生产者 sync-flag 偏移位于消息头;描述符环区域和消费者标志位于 per_core 向量中。生产者和消费者寻址 sync flags 与环窗口所需的一切都能从这一条消息到达。
编码
字段映射从嵌入的 FileDescriptorProto 精确按字节切出(DescriptorProto body @ 0xc18e12d,len 0x289=649 的 protoc --decode_raw),并由生成的 TcParseTable 成员偏移数组 TpuChipConfigProto_ContinuationQueue::_table_ @ 0x2200cb48(12 字节 FieldEntry stride {u32 member_offset, u16 has_idx, u16 typecard})和 Clear() @ 0x20b0c5a0 佐证。
| Field # | 名称 | Proto 类型 | C++ off |
|---|---|---|---|
| 1 | core_type | enum .tpu.TpuCoreTypeProto(TC=0 / BarnaCore=2) | +0x28 |
| (2) | —(不存在 — 字段编号空洞) | — | — |
| 3 | completion_state_sync_flag_word_offset | int32 | +0x2c |
| 4 | error_ack_sync_flag_word_offset | int32 | +0x30 |
| 5 | overwritten_error_sync_flag_word_offset | int32 | +0x34 |
| 6 | completion_interrupt_number | int32 | +0x38 |
| 7 | per_core(repeated ContinuationQueue.PerCore) | message | +0x18(count +0x20) |
| 8 | producer_sync_flag_min_descriptor_word_offset | int32 | +0x3c |
| 9 | producer_sync_flag_remaining_descriptors_word_offset | int32 | +0x40 |
| 10 | producer_sync_flag_count | int32 | +0x44 |
| 11 | producer_sync_flag_index_word_offset | int32 | +0x48 |
has_bits 位于 struct+0x10。四个生产者 sflags(#8–#11)是顶层 ContinuationQueue 字段,而不是逐 core 字段 — 每个 core 已有一份队列配置,所以逐队列字段本身已经是逐 core 的。
注意 — 四个
producer_sync_flag_*字段是ContinuationQueue字段 #8/#9/#10/#11;PerCore只持有 #1/#2。设备生产者(EmitContinuationTailcall,§4)从位于config+0x0/+0x4/+0x8/+0x1c的逐 core 队列头读取 producer base/remaining/count/index — 这是字段 #8/#9/#10/#11 的运行时头部重布局。
PerCore 子消息
ContinuationQueue.PerCore(DescriptorProto @ 0xc18e326,len 138;_table_ @ 0x2200cab8)恰好携带两个 message 类型字段:
| Field # | 名称 | Proto 类型 | C++ off |
|---|---|---|---|
| 1 | shared_memory_region | message .tpu.TpuChipConfigProto.SharedMemoryRegion | +0x18 |
| 2 | consumer_sync_flag | message .asic_sw.deepsea.SyncFlag | +0x20 |
SharedMemoryRegion(DescriptorProto @ 0xc18dd41,len 117;_table_ @ 0x2200c638)命名描述符环窗口:
| Field # | 名称 | Proto 类型 | C++ off |
|---|---|---|---|
| 1 | shared_memory | message .tpu.TpuSharedMemoryOnChipProto(HBM / CMEM kind) | +0x18 |
| 2 | word_count | INT64 | +0x20 |
| 3 | word_offset | INT64 | +0x28 |
注意 —
word_count/word_offset在 proto 中是int64。运行时Target的MemoryPart携带截断后的int32副本(+0x04=word_offset,+0x08=word_count),因为片上字偏移适合 32 位;运行时 ctor 中的环窗口算术(§3)读取这些int32副本。不要在 proto 层假设int32。
consumer_sync_flag 是标准 asic_sw.deepsea.SyncFlag 句柄(DescriptorProto @ 0xc19228a,len 226)— 即设备在读取描述符槽之前等待的 sync flag:
| Field # | 名称 | Proto 类型 | 角色 |
|---|---|---|---|
| 1 | index | int32 | core 的 tier 内的 sflag 字索引 |
| 2 | core | enum Core | 哪个 engine 拥有该标志 |
| 3 | core_index | int32 | 逐 core 实例索引 |
| 4 | tile_index | int32 | tile 索引(SparseCore tiles) |
嵌套 Core enum 是 {TENSORCORE=0, BARNACORE=1, HOST_INTERFACE=2, SPARSECORE=3, SPARSECORE_TAC=4, SPARSECORE_TEC=5}。(这与 SparseCore sequencer enum 使用的 SCS/TAC/TEC 编号相同;构建中还存在另外两个不同的 SyncFlag 消息 — {is_host, word_offset} @ 0xc18de3b 和 {word_offset, value, expected_transfer_size} @ 0xc1792af — 它们是不同类型,不是 consumer flag。)
运行时配置区域头
当 RegisterContinuationQueueConfigs 将 proto 复制到 Target 中时,设备生产者索引的头部重布局(Target+0x580 + core*0x38,stride 0x38)暴露四个生产者 sflags 加上 per_core 向量:
| Off | 来源字段 | 角色 |
|---|---|---|
+0x00 | #8 …min_descriptor_word_offset | 生产者描述符数组 base sflag("…available_count_sync_flag_base",len 0x31) |
+0x04 | #9 …remaining_descriptors_word_offset | remaining-descriptors sflag base("…_remaining_descriptors_base",len 0x47) |
+0x08 | #10 …count | 环槽位数量 — 2 的幂(popcnt==1 CHECK'd) |
+0x1c | #11 …index_word_offset | 生产者写索引 sflag("…available_count_sync_flag_index",len 0x32) |
+0x20 | #7 per_core | vector .begin(TC core 0 为 Target+0x5a0) |
+0x28 | #7 per_core | vector .end(TC 为 Target+0x5a8) |
每个 per_core 元素(28 字节的 Target::ContinuationQueueConfig::PerCore,stride 0x1c):+0x00 MemorySpace,+0x04 word_offset,+0x08 word_count,+0x0c consumer-sflag ptr(可选),+0x14 has-consumer bool,+0x18 producer-sflag word offset。C++ 成员类型名 Target::ContinuationQueueConfig::PerCore 是从生产者溢出路径引用的 std::vector<…ContinuationQueueConfig::PerCore>::__throw_length_error 符号 @ 0x1271a660 恢复的。
2. 异步描述符记录
目的
“continuation descriptor” 是生产者 DMA 的数据,用于让下一个程序知道要运行什么。它不是带显式字节偏移的打包 struct — 它是一个扁平 int32 字数组(逐程序 SMEM 控制块),其中每个程序控制字段占用 TpuMemoryReservationType 预留的字槽。主机逐字段填充它;设备通过 §4 中的 Target 字偏移访问器读回同一块内存。每个入队程序对应一条记录。
布局
ReservedSmemFiller::FillCoreBuffer(TpuCore*, TpuRunConfig const&, Inputs const&, bool, Span<int>) @ 0x1d4c1d60 写入记录。每次字段写入都经过 lambda $_0(TpuMemoryReservationType type, int value, int idx) @ 0x1d4c2860,它执行 span[GetRegionForType(type).word_offset] = value。GetRegionForType @ 0x20b16260 在 [reservation+0x4c0 + type*0x18] 处索引逐代 region table,并返回 24 字节 {word_offset, word_count, …} 区域;type enum 有 50 个条目(< 0x32)。
已验证的写入集合(每行是一个 int32 槽;设备读回访问器位于 Targets overview 的 program-descriptor SMEM block,Target+0x808..+0x8b8):
TpuMemoryReservationType (idx) | 值来源(FillCoreBuffer / Inputs) | 设备读回(Target off) |
|---|---|---|
kXprofProgramId (0x07) | TpuRunConfig::xprof_program_id() | ProgramIdLocation (+0x808) |
kRunIdLow (0x08) | Inputs+0x08 | RunIdLowLocation (+0x820) |
kRunIdHigh (0x09) | Inputs+0x0c | RunIdHighLocation (+0x828) |
kSameAsLastProgram (0x12) | 0 / cache-hit flag | SameAsLastProgram (+0x838) |
kLaunchBarrierId (0x13) | 0 | LaunchBarrierId (+0x840) |
kCrossProgramPrefetchSuccess (0x14) | region / flag | CrossProgramPrefetchSuccess (+0x848) |
kProgramDescriptorSize (0x15) | descriptor_bytes / words-per | ProgramDescriptorSize (+0x858) |
kProgramDescriptorState (0x16) | Inputs+0x4c(1=initial / 2=continuation) | ProgramDescriptorState (+0x860) |
kProgramEntryPointAddress (0x17) | Inputs+0x38(下一个程序入口地址) | ProgramEntryPointAddress (+0x868) |
kProgramEntryPointSize (0x18) | Inputs+0x3c(入口大小) | ProgramEntryPointSize (+0x870) |
kHbmStackOffset (0x05) / kCmemStackOffset (0x06) / kHostStackOffset (0x21) | region word_offset(栈基址) | — |
kHbmOffset (0x03) / kCmemOffset (0x04) | GetHeapWordOffset(., HBM/CMEM) | heap slots |
kTensorCoreStackSize (0x1d) / kSparseCoreStackSize (0x1e) | 逐代栈大小(以字计) | — |
kTrapId (0x23) | trap-id(若无则为 0) | — |
kSparseCorePTState (0x30) | 0xFFFFFFFF(unset sentinel) | — |
kTensorCoreAssertionArgs (0x31) | 0xC0C0C0C0(poison fill) | — |
kProgramDescriptorState / kProgramEntryPointAddress / kProgramEntryPointSize 写入已按字节确认:$_0(span, 22, *(Inputs+19), 0)(Inputs+0x4c)、$_0(span, 23, *(Inputs+14), 0)(Inputs+0x38)、$_0(span, 24, *(Inputs+15), 0)(Inputs+0x3c);poison 填充是 $_0(span, 48, 0xFFFFFFFF, …) 和 $_0(span, 49, 0xC0C0C0C0, …)。
注意点 — 该记录没有 framing word、没有 version word、没有 field-tag bytes。它就是裸的
int32SMEM 控制块;“哪个字段”完全由某个 reservation type 拥有的哪个字槽来编码。重新实现者如果没有逐代 reservation table(GetRegionForType),就无法解析该映像。记录结构(type→slot)在此已按字节精确确认;每个槽的字面字偏移是代际相关的(来自 chip-parts table)。consumer sync-flag 不在记录中 — 它作为WriteMemory的optional<SyncFlagInfo>参数在带外携带(§3)。
记录字节大小
ReservedSmemFiller::GetRequiredDescriptorBytes(TpuCore*, TpuRunConfig const&) @ 0x1d4c2a20:
// tpu::ReservedSmemFiller::GetRequiredDescriptorBytes sub_1D4C2A20
function GetRequiredDescriptorBytes(core, run_config):
count = chip_config.producer_sync_flag_count // [chipcfg+0xc8]
granule = (count >= 513) ? count : 512 // alignment granule, floored at 512
reserved = run_config[+0x28] + user_region.word_count // reserved int32 word count
- user_region.word_offset_pair // (GetUserRegion offsets)
bytes = reserved * 4 // int32 words -> bytes
rem = bytes % granule
return (rem == 0) ? bytes : bytes + (granule - rem) // round bytes UP to a multiple of granule映像由 MaybeMappedBuffer functor 分配:先用 PremappedMemoryManager::Allocate(固定且 DMA 可映射的区域 — 返回设备字节偏移 [cont+0xc0]-[[cont+0xb8]+8]),失败时回退到 ContinuationDescriptor::DefaultAllocator @ 0x1d6272e0(posix_memalign(&p, 0x20, size),32 字节对齐,free deleter)。ContinuationDescriptor ctor 会把整个映像 memset 为 0,然后 fill functor(通过 __bind_front 绑定的 FillCoreBuffer)写入字段。
怪癖 — 常量
0x200=512 出现三次,始终作为下限,绝不是 in-flight 上限:在这里作为字节大小对齐 granule(max(count, 512));作为运行时min_descriptor_size_下限(obj+0x58 = max(descsize, 512),§3);以及在ContinuationDescriptor::Terminator(int)@0x1d627260中的if (a2 < 513) a2 = 512;clamp。因此描述符映像永远不会小于 512 字节,而Terminator描述符(结束链的描述符)继承同样的 512 字节最小值。应将 512 视为描述符映像的最小大小,而不是三个独立的魔数 — 也不是 in-flight 描述符数量限制。
3. 运行时环 {#3-the-runtime-ring}
目的
tpu::ContinuationQueue 是拥有描述符环的主机侧生产者/消费者。它是带锁保护的队列,并有一个异步工作线程:XLA 侧 Enqueue 一个请求,工作线程弹出它并派发延续(延续会把描述符映像 DMA 到设备 SMEM),设备中断驱动 Completed。设备 SMEM 中的环窗口是所选 core 的 PerCore.shared_memory_region 给出的 [word_offset, word_offset + word_count)。
对象布局
来自 tpu::ContinuationQueue::ContinuationQueue(...) ctor @ 0x1d160ae0(per_core[core] 元素由 TpuCoreOnChip 参数的 int32 @+0x4 选择,vector stride 0x30):
| Off | 字段 | 来源 / 角色 |
|---|---|---|
+0x00 | config head(ymm copy) | ContinuationQueue descriptor +0..+0x1f |
+0x20 | int32 | descriptor +0x20 |
+0x28/+0x30/+0x38 | per_core vector {begin, size, cap} | descriptor per_core 的堆副本 |
+0x40 | 环窗口起点字节 | per_core[core]+0x10(word_offset) * wordsize |
+0x48 | 环窗口终点字节 | (per_core[core]+0x8(word_count) + +0x10) * wordsize |
+0x50 | granule_bytes_ | 原始 descriptor-size ctor 参数(所有三个 % granule_bytes_ CHECK 中的除数) |
+0x58 | min_descriptor_size_ | max(arg, 0x200=512) — 512 字节下限(CHECK "min_descriptor_size_ % granule_bytes_ == 0") |
+0x60 | max_descriptor_size_ | (END − START)/2 − descsize(CHECK "max_descriptor_size_ % granule_bytes_ == 0") |
+0x68 | TpuHostWorkQueue* | ctor 参数 |
+0x70 | core_type / completion interrupt | descriptor +0xc |
+0x78 | 用户 dispatch function | std::function 参数 |
+0x88 | WriteMemory writeback functor | ctor 参数 |
+0xa0 | worker thread | 由 Initialize 设置(0x160-B state) |
+0xa8 | absl::Mutex(队列锁) | Enqueue/WorkerLoop/Completed |
+0xb0 | SyncFlagRefcounts | 若 count > 1 则构造;valid-flag +0xc8 |
+0xe0..+0x100 | in-flight std::deque<Request> + count | +0x100 = producer count(在 Enqueue 时递增) |
+0x110..+0x130 | completed container + idx | +0x130 = consumer index(在 dispatch 时递增) |
+0x1b0 | running flag | 由 Initialize 置 true,供 WorkerLoop 轮询 |
ctor 的环窗口算术按字节证明 per_core+0x10 是 word_offset,per_core+0x8 是 word_count:START = word_offset * wordsize,END = (word_count + word_offset) * wordsize — 一个半开 [base, base+size) 区域。反编译显示 obj+0x60 = v24/2 - descsize(容量)以及一个 CHECK(descriptor_state_word_offset_ < min_descriptor_size_ / sizeof(int32_t)),确认记录以 int32 字计量大小。
Drain 协议
// the host-side producer/consumer drain
function Enqueue(descriptor, device_byte_offset, completion_cb): // sub_1D161160
// bounds-check the descriptor byte offset against [min_descriptor_size_, max_descriptor_size_]
if descriptor[+0x28] > obj[+0x60] || descriptor[+0x28] < obj[+0x58]:
completion_cb(OutOfRange status); return // failure path
append Request{descriptor, completion_cb, buffer functors} to in-flight deque
obj[+0x100]++ // producer count
post deferred write onto host work queue obj[+0x68]
function WorkerLoop(): // sub_1D162BA0
acquire obj[+0xa8] (mutex 0x21146de0)
while obj[+0x1b0] (running): // polled flag
swap out the in-flight deque<Request>
for each Request req:
(*req[+0x58])() // continuation dispatch -> drives WriteMemory -> DMA
(*req[+0x50])() // completion callback
obj[+0x130]++ // consumer index
release mutex (0x21147b40)
function WriteMemory(dev_off, image:Span<uchar>, len, sync_flag_info, cb): // sub_1D163A40
if obj[+0x70] (core_type) == 2: ... // host-interface gate
memcpy the descriptor image into the shared-memory ring // sub_1D163DD4
post the write through host work queue with the optional SyncFlagInfo
invoke writeback functor obj[+0x88]
function Completed(int idx, bool ok): // sub_1D1616A0 (device-VInt ISR)
acquire obj[+0xa8]
walk the completed container [obj+0x130], match descriptor by idx
fire its completion callback (req[+0x50]); free the slot
emplace follow-ons (EmplaceBackSlow 0x1d164d60)
// ok=false maps to the error_ack / overwritten_error sflag roles (config #4/#5)in-flight 容器是 std::deque<Request>;每个 Request 为 0x80 字节(EmplaceBackSlow 执行 shl $0x7)。Request 携带 ContinuationDescriptor(MaybeMappedBuffer ptr@+0、deleter +8、span-fill fn +0x10、mapped-buffer state +0x18、ring byte offset +0x28)、一个 WriteMemory functor +0x40、一个 completion callback +0x50,以及 continuation dispatch functor +0x58。
入队驱动器
tpu::RealProgramContinuator::EnqueueProgram @ 0x1d153ca0 是更高层的驱动器,它构建描述符并调用 Enqueue。它调用 GetRequiredDescriptorBytes(→ size)、memcpy 程序映像、构建一个 0x140 字节的 Inputs struct 并通过 __bind_front 绑定到 FillCoreBuffer、构造 ContinuationDescriptor(size, fill_fn, alloc_fn)(@ 0x1d627220 — 调用 fill(Span<int>{buf, size>>2}) 的那个 variant),然后调用 Enqueue(descriptor, device_byte_offset, completion_cb),其中 offset 是 MaybeMappedBuffer 设备字节偏移($_3+0x30)。ProgramContinuator 暴露 AttachOnEnqueue/AttachOnComplete/AttachOnError hooks(@ 0x1d1453e0/0x1d145460/0x1d1454e0),并连接到它自己的 FSM(Init/Working/Draining/Drained/TearingDown/TearedDown,operator<< @ 0x1d145600)— 这是一个与设备侧描述符 State 字(§4)不同的状态机。
注意 — 主机侧选择
kProgramDescriptorState= 1(initial)还是 2(continuation)的位置是Inputs+0x4c;在入队时设置它的单一谓词尚未隔离(Inputs+0x4c的来源证据为 LOW;对记录的写入已 CONFIRMED)。Megacore barrier 处的设备侧SetProgramDescriptorState(2)已按字节确认(§4)。
4. 设备侧生产者环推进 {#4-the-device-side-producer-ring-advance}
目的
运行时环的编译期对应物是 EmitContinuationTailcall — 这是 continuator program 在设备上运行的 LLO,用于推进生产者索引 sync flag、寻址描述符数组 sflags、从 SMEM 控制块读出下一个程序的入口点、DMA 描述符、触发主机中断,并执行尾调用。这就是在设备侧实际 drain 一个环槽并把控制权交给后继程序的机制。
入口点
TpuCompactionIsaEmitterCodegen::Create (bit-0 gate @0x1090eb6e)
└─ deepsea_compiler_backend::CompileContinuationTailCall @0x10a28b00 ── builds a fresh continuator program
├─ EmitContinuationTailcall @0x12718ca0 ── the ring-advance + DMA + tailcall body
└─ ShaltInternal @0x10a28d21 (iff Emit returned rax==1) ── the continuator's OWN closing halt算法
// xla::jellyfish::continuations::EmitContinuationTailcall(LloRegionBuilder, long) sub_12718CA0
function EmitContinuationTailcall(rb, nop_count):
core = TpuCoreTypeForSequencer(module[+0x268])
cfg = Target + 0x580 + core*0x38 // the per-core ContinuationQueueConfig head
// precheck: platform==iss & Megachip & CoresPerChip(SC)>0 & core==2 & Target+0x628 bit-0
for nop_count iterations: Vnop(); CompilerBarrier() // overlay-prelude padding
idx_ptr = SflagImmPtr(cfg[+0x1c], "continuation queue available_count_sync_flag_index", 50)
idx = VsyncRead(idx_ptr) // current producer write index
CHECK(absl::has_single_bit(cfg[+0x8])) // count must be power-of-2 (popcnt==1)
new_idx = SandU32(SaddS32(idx, 1), cfg[+0x8] - 1) // (idx+1) & (count-1) — ring wrap
VsyncSet(idx_ptr, new_idx) // advance the producer index sflag
base_ptr = SflagImmPtr(cfg[+0x0], "continuation queue available_count_sync_flag_base", 49)
base = CalcWordAddr(base_ptr, idx) // descriptor-array base, indexed by idx
rem_ptr = SflagImmPtr(cfg[+0x4], "...available_count_sync_flag_remaining_descriptors_base", 71)
rem = CalcWordAddr(rem_ptr, idx) // remaining-descriptors sflag
// per_core loop: cfg[+0x20]..cfg[+0x28], stride 0x1c; each entry checked (+0x14==1 && +0xc==0)
state = Sld(SmemWordImmPtr(target.ProgramDescriptorStateWordOffset())) // Target+0x860
addr = Sld(SmemWordImmPtr(target.ProgramEntryPointAddressWordOffset())) // Target+0x868 (next program)
size = Sld(SmemWordImmPtr(target.ProgramEntryPointSizeWordOffset())) // Target+0x870
run_id= Sld(SmemWordImmPtr(target.RunIdLowLocationWordOffset())) // Target+0x820
EnqueueDmaLocalInGranules(...) // sub_1D540640 — DMA the descriptor
VsyncAdd / VwaitEqSV / VwaitDone // descriptor-ring handshake
VInt(0x80000000) // host interrupt -> Completed
tailcall(addr, size) // jump into the next program
return status // rax==1 -> continuator emits its closing Shalt__popcnt(cfg[+0x8]) != 1 检查以及断言字符串 "absl::has_single_bit(continuation_queue.available_count_sync_flag_count)" 已在反编译第 310 行按字节确认;SflagImmPtr(cfg[+0x1c], "…_index", 50) / SflagImmPtr(*cfg, "…_base", 49) / SflagImmPtr(cfg[+0x4], "…_remaining_descriptors_base", 71) 读取和 (idx+1)&(count-1) 环绕也都字节精确。core*0x38 + 0x580 索引(v6 = v3 + 56*v5 + 1408)确认了 0x38 的头部 stride。
描述符 State 握手
设备侧 ContinuationDescriptor::State 字(位于 Target+0x860 的 SMEM)是一个 2 值握手:
SetProgramDescriptorState(tpu::ContinuationDescriptor::State, rb)@0x1271a5e0将SimmU32(State)Sst到该槽。所有三个调用点(全二进制)都在BarrierCoresWithIdVerificationInternal@0x12715c00内(0x12717828/0x12718183/0x1271825b),并且每个都传入mov $0x2,%edi— 即 State=2。另外,SynchronizeProgramDescriptorStatesMegacore@0x1c697540不调用SetProgramDescriptorState;它读取ProgramDescriptorStateWordOffset处的 State 字并将其EnqueueRemoteSst到 twin core — 是在 Megacore 对之间传播(而不是设置)State。GetProgramDescriptorState(rb)@0x1271a580Sld它。LowerHloModuleImpl读取它,并基于(State SeqS32 1)对下一个 block 加谓词。
所以 State 1 = 第一次/初始运行,State 2 = continuation/下一个程序就绪(Megacore barrier 会在尾调用前把两个 twin core 的 State 同步为 2)。该构建中没有独立的 enumerator names 描述符;只观察到了字面值 1 和 2。
怪癖 — 两个无关状态机共享 “State” 一词。设备描述符
State(本节,值 1/2)是 continuator 读取以决定是否对 continuation block 加谓词的内容。主机tpu::ProgramContinuator::State(Init/Working/Draining/…,operator<<@0x1d145600,§3)是驱动器 FSM。二者不同;不要混淆。主机 FSM 驱动Enqueue;设备State字 gate 片上谓词。
5. Scalar-Halt 映射
目的
程序终止是一个拆分程序模型。没有延续队列时,程序以阻塞式 scalar-halt 结束,由主机提交下一个程序。启用队列后,尾部 halt 被抑制,一个单独的 continuator program 会推进队列并尾调用后继。本节映射每个 scalar-halt 发射点以及切换它们的 gates,让重新实现者准确知道何时发射、抑制或替换 halt。
Halt opcode
scalar-halt 是 LloOpcode 0x25,只由 LloRegionBuilder::ShaltInternal @ 0x1d520d20 发射(= CreateNullaryOp(0x25) + AppendInstruction)。opcode 名称来自 LloOpcodeString 表 @ 0x21cd0d60(R_X86_64_RELATIVE addends):
| Opcode | 字符串 |
|---|---|
0x25 | "scalar-halt" |
0x26 | "scalar-halt-yield-cond"(本构建中未作为 nullary 发射) |
0x27 | "scalar-halt-on-error"(LloRegionBuilder::ErrorIf / Error — error halt,不是程序末尾) |
程序末尾发射 / 抑制点
五个 ShaltInternal 调用者,全二进制:
| # | 调用者 | Site VMA | Gate / 角色 |
|---|---|---|---|
| 1 | barna_core::BcsLloProgramCreator::Build() | 0xf9ce922 | unconditional — BarnaCore sequencer LLO 程序末尾 |
| 2 | barna_core::BcsLloProgramCreator::BuildTop() | 0xf9cebdd | unconditional — BarnaCore 顶层程序末尾 |
| 3 | DeepseaCompilerBase::LowerHloModuleImpl() | 0x10920035 | 逐 sequencer gate(见下)— 主 HLO lowering 程序末尾 |
| 4 | DeepseaCompilerBase::CompileInternal() | 0x10928095 | test [Target+0x628],1; jne skip — bit-0 SET 会抑制 main halt |
| 5 | deepsea_compiler_backend::CompileContinuationTailCall() | 0x10a28d21 | continuator 自己的收尾 halt(当且仅当 EmitContinuationTailcall 返回 rax==1) |
Target+0x628 bit-0 使用者
Bit-0(存在 TC 延续队列区域)在三个位置测试:
| Site VMA | 外层函数 | bit-0 SET 时的动作 |
|---|---|---|
0x10928083 | CompileInternal | 跳过尾部 ShaltInternal(main program 不发射 halt) |
0x1090eb6e | TpuCompactionIsaEmitterCodegen::Create | 发射 continuator:调用 CompileContinuationTailCall @ 0x1090eba0 |
0x1091bcc7 | LowerHloModuleImpl | 一个邻近描述符状态谓词的 testb $0x4 IsMegachip 复查 |
因此 bit-0 同时做两件事:它抑制 main program 的 halt,并且启用 continuator 的编译。continuator(CompileContinuationTailCall)构建自己的 LloRegionBuilder,发射生产者环推进 + 描述符 DMA + 尾调用(EmitContinuationTailcall),并以自己的 Shalt 结束(已按字节确认:反编译显示 EmitContinuationTailcall(...),然后是一个条件性 ShaltInternal)。
LowerHloModuleImpl 逐 sequencer gate
上面的 site #3 使用比 CompileInternal 的简单 bit-0 测试更丰富的谓词(字节精确 @ 0x1091ffc0..0x10920035):
// the program-end halt decision in LowerHloModuleImpl
if Megachip() && CoresPerChip(SC) > 0 && sequencer == 2 (BarnaCore)
&& (((Target+0x628 bit-2) >> 2) | Target+0x540) & 1:
emit Shalt // megachip BarnaCore: blocking halt
else:
bit = (1 << sequencer) & Target+0x628
if bit == 0 || sequencer < 3:
emit Shalt // no continuation-queue bit for this sequencer
else: // bit set && sequencer >= 3
SKIP Shalt // continuation queue present for this sequencer这里 sequencer 是 TpuSequencerType 逐 core 索引,Target+0x540 是 platform==iss。延续队列是否存在是一个逐 sequencer bit (1 << sequencer) & Target+0x628,不是单一全局标志 — Megachip 运行可以有些 sequencer halt,另一些继续。
注意点 — “halt”和“continuation”在芯片级并不互斥。把 bit-0 当成“任何地方都不 halt”的幼稚重新实现是错误的:main program 不发射 halt,但 continuator 仍然以
Shalt结束,并且任何(1 << sequencer)bit 未置位的 sequencer 仍会正常 halt。延续链中的终端程序 — 即没有已入队后继的程序,也就是Terminator描述符 — 才是真正 halt 整条链的那个。
相关组件
| 名称 | 关系 |
|---|---|
tpu::RealProgramContinuator | 构建描述符并调用 Enqueue 的主机驱动器;拥有队列和 OnEnqueue/OnComplete/OnError hooks |
tpu::ReservedSmemFiller | 渲染描述符记录(FillCoreBuffer)并计算其大小(GetRequiredDescriptorBytes) |
xla::jellyfish::continuations::EmitContinuationTailcall | 设备侧 LLO,用于 drain 一个槽、DMA 记录并尾调用 |
DeepseaCompilerBase / TpuCompactionIsaEmitterCodegen | 编译期 gate(Target+0x628 bit-0),用于抑制 main halt 并发射 continuator |
交叉引用
- 芯片内 DMA 描述符 —
(mem_id, core_id)内存空间编码、src/dst opcode 枚举,以及实际片上 DMA(这里由EnqueueDmaLocalInGranules发出)使用的 OCI 描述符字段布局;本页不重复这些枚举 - UHI 主机接口 — 将 continuation descriptor 映像实际移动到设备 SMEM 的主机↔设备传输
- 主机↔设备 DMA — 延续队列的记录传输所依附的主机路径 DMA 分类(
DMA_TYPE_CHIP_TO_HOST/DMA_TYPE_LOCAL_OR_HOST) - OCI 命令 DMA-ID — 用于 profile continuation DMAs 的描述符 begin/end trace-id 配对
- SFLAG 协议 — producer index / remaining-descriptors / consumer flags 所在的 sync-flag tier
- SMEM 标量内存 — 扁平
int32描述符记录和 program-descriptor 控制块占用的标量内存 tier - 内存概览 — 更广泛片上内存模型中的延续队列小节
- Targets 概览 — 记录在设备上通过其读回的 program-descriptor SMEM 控制块(
Target+0x808..+0x8b8)