TpuTransferManager 名册
本页所有地址均适用于来自
libtpu-0.0.40-cp314wheel 的libtpu.so(构建libtpu_lts_20260413_b_RC00,build-id md589edbbe81c5b328a958fe628a9f2207d,781,691,048 字节,ELF x86-64 DYN,未剥离;反修饰后的 C++ 符号和 IDA 恢复的 C 名称均按原样引用)。.textVMA 等于文件偏移。其他版本会不同。
摘要
TpuTransferManager_* 是支撑 TfTpu C-API shim 中跨 xla::TransferManager 的 C-ABI 函数组。它是主机↔设备的数据移动表面:把主机上的 xla::Literal 编组进 TPU xla::ShapedBuffer(以及反向),推送/拉取流式 infeed/outfeed 字面量,查询某个形状在设备端的字节大小,把字面量线性化为原始设备布局缓冲区,并回答两个“我现在能否触碰这个缓冲区”的谓词。IDA 从 .rodata 引用和调用目标恢复出十九个 extern "C" 自由函数,它们都命名为 TpuTransferManager_<Method>,并且都位于 .text 中紧凑的 0xeaba0a0–0xeabb827 区间内(一个例外是 GetInfeedLayout @ 0xf6a1a80,位于不同翻译单元)。IDA 源路径字符串 learning/45eac/tfrc/executor/stream_executor/tpu_transfer_manager_c_api.cc(GetInfeedLayout 第 28 行)锁定了该函数组的来源文件。
定义性的结构事实,也是把这个名册同 TpuExecutor_* 区分开的事实,是 C 函数如何抵达真正实现。TpuExecutor_* 函数接收一个不透明的 SE_StreamExecutor*,并通过它的 vtable 分派进 deepsea 驱动。TpuTransferManager_* 函数不接收 executor 句柄;相反,每个函数都会即时解析 TPU 平台的单例 xla::TransferManager:GetRegisteredDeepseaPlatform()(缓存在 GetUnderlyingDeepseaPlatform::platform Meyers-static guard 后面)→ xla::TransferManager::GetForPlatform(platform) @ 0x1342f180(一个 StatusOr<TransferManager*>),然后通过该 manager 的 C++ vtable 中的固定字节偏移弹跳调用。因此整个函数组是一个先解析、再弹跳的 shim:用 ApiConverter::FromC 把 C 结构编组进 C++,查找每个平台的 TransferManager 单例,调用一个 vtable 槽位,用 ApiConverter::ToC 把结果编组回来,并销毁临时对象。
本页负责函数名册 + 每个函数的实现符号 / vtable 槽位映射。不透明句柄 / ApiConverter::ToC/FromC 约定以及三张 *ApiFn 表的访问器模型见 shim 概览。运行时层面的 infeed/outfeed 队列机制,即 PJRT 原生的 tpu::System 路径、span 分块、阻塞语义、片上队列驱动,见 Infeed / Outfeed Queues;这里的两个 TpuTransferManager_TransferLiteral{ToInfeed,FromOutfeed} 行是进入该子系统的遗留 StreamExecutor 入口,该页面也把它们记录为遗留半边。设备端 ShapedBuffer 所属的 PJRT 缓冲区/内存 ABI 见 PJRT Buffer & Memory。
对于重新实现,契约是:
- 先解析、再弹跳惯用法 — 不传递
SE_*句柄;每个函数都会解析每个平台的xla::TransferManager单例(GetForPlatform @ 0x1342f180)并调用一个 vtable 槽位。C++TransferManagervtable 偏移就是分派键。 - 编组纪律 — 每个作为 shape / layout / literal / shaped-buffer 的 C 参数,都会在弹跳调用前通过
ApiConverter::FromC转成栈上的 C++ 对象,并在之后调用~Dtor;out-param 通过ApiConverter::ToC回写;absl::Status结果会作为写入调用方 out 指针的引用计数StatusRep*返回。 - vtable 槽位映射 — 下表把每个 C 函数固定到它的
xla::TransferManager虚函数偏移(+16,+24,+32,+40,+48,+56,+64,+72,+80,+88,+104,+112,+120,+136),这是重新实现者必须保持字节稳定的唯一关键。 - 两个非 vtable 成员 —
New/Free是平凡堆操作,而LinearizeToBuffers/GetInfeedLayout完全绕过 manager vtable,直接基于解析出的TpuTopology调用xla::jellyfish线性化器 /TransferSizeUtil。
| 名册大小 | 19 个 extern "C" TpuTransferManager_* 自由函数(与概览计数一致) |
| 地址区间 | 0xeaba0a0–0xeabb827 连续(18 个函数;ReadDynamicShapes 结束于 0xeabb827,随后是 TpuComputationPlacer_New @ 0xeabb840)+ GetInfeedLayout @ 0xf6a1a80(离群 TU) |
| 背后的 C++ 类 | xla::TransferManager(TPU 子类;每次调用解析,不持有) |
| 单例解析 | GetRegisteredDeepseaPlatform → xla::TransferManager::GetForPlatform @ 0x1342f180 (StatusOr) |
| 平台缓存 | GetUnderlyingDeepseaPlatform::platform(函数局部 static,受 __cxa_guard 保护) |
| 分派键 | C++ xla::TransferManager vtable 字节偏移(*(*manager + off))(manager, …)) |
| 编组 | ApiConverter::FromC(入)/ ToC(出);xla::Shape / Layout / LiteralSlice / MutableBorrowingLiteral / ShapedBuffer 临时对象 |
| 状态输出 | 写入调用方 out-ptr 的 absl::status_internal::StatusRep*,旧值会 Unref |
| 抵达方式 | ExecutorApiFn 表槽位(由 TfTpu_Initialize Bootstrap 填充) |
| 来源文件 | learning/45eac/tfrc/executor/stream_executor/tpu_transfer_manager_c_api.cc |
| 证据等级 | 可重新实现级 / 已按 IDA 反编译字节确认(已检查 19/19 个函数体) |
范围 — 指向这些函数中每一个的逐函数
ExecutorApiFn槽位(以及何时填充)属于 TfTpu_Initialize Bootstrap。infeed/outfeed 对最终落到的流式队列机制由 Infeed / Outfeed Queues 负责。本页只记录 C-ABI 名册和 C++ vtable 弹跳调用。
1. 每个函数的先解析、再弹跳形态
目的
每个非平凡的 TpuTransferManager_* 函数都有同一个骨架,重新实现者只要内化一次,就能通过检查槽位偏移和参数编组读懂全部十四个 vtable 后备成员。这里没有需要穿线传递的逐调用 executor 句柄;平台是进程全局单例。
算法
// canonical body — sub_EABA* (e.g. GetByteSizeRequirement @ 0xeaba4c0)
function TpuTransferManager_<Method>(<C args...>):
// 1. marshal every rich C struct arg into a stack C++ object
Shape host_shape; ApiConverter::FromC(&host_shape, c_shape) // 320-byte xla::Shape
// (literals → MutableBorrowingLiteral/LiteralSlice; buffers → 784-byte xla::ShapedBuffer)
// 2. resolve the per-platform TransferManager singleton (cached after first call)
if !GetUnderlyingDeepseaPlatform::platform.guard: // __cxa_guard_acquire
GetUnderlyingDeepseaPlatform::platform =
deepsea::executor::GetRegisteredDeepseaPlatform() // one-time
StatusOr<TransferManager*> mgr =
xla::TransferManager::GetForPlatform(platform) // 0x1342f180
if mgr.is_error(): // payload ptr != &dword_0+1
absl::internal_statusor::ThrowBadStatusOrAccess(mgr) // never returns on TPU
// 3. bounce through ONE C++ vtable slot — the offset is the dispatch key
TransferManager* m = mgr.value();
result = (*(*(void**)m + <VTABLE_OFFSET>))(m, <unwrapped args>, host_shape)
// 4. marshal results back out; destroy temporaries (reverse order)
ApiConverter::ToC(&out_cpp, c_out) // for out-param functions
xla::Shape::~Shape(&host_shape) // every FromC'd temp gets its ~Dtor
return result
```text
`mgr.is_error()` 检查是恢复出的 `StatusOr` 惯用法:成功哨兵是等于 `&dword_0 + 1` 的 payload 指针(带标记的 “ok” 值)。其他任何值都是状态,因此代码会引用/抛出 `ThrowBadStatusOrAccess`。在正确初始化的 TPU 平台上 manager 总能解析成功,所以该分支实践中是死分支,但为了 ABI 对等必须复现。
### 为什么没有 executor 句柄
上游 XLA 中的 `xla::TransferManager` 是一个从静态注册表获取的*每平台*对象(`TransferManager::GetForPlatform`),不是逐设备对象。TPU 构建保留了这个模型:进程中恰好有一个 TPU `TransferManager`,所以 C shim 没有设备特定内容可传,而是自行解析单例。对比 [`TpuExecutor_*`](tpu-executor-roster.md):那里 executor *就是*逐设备句柄,必须穿线传入每次调用。
> **QUIRK —** manager 指针永远不会由这些函数释放。`New`/`Free` 分配和释放一个 1 字节占位符(见 §2),主机把它当作“transfer manager 句柄”,但*真正的* `xla::TransferManager` 是每次调用解析出的进程单例,并且比所有句柄都活得更久。试图把设备状态存进 `New` 出来的对象的重新实现者会发现它只是哑对象;所有状态都在单例里。
---
## 2. 生命周期(`New` / `Free`)
### 函数映射
| 函数 | 地址 | 大小 | 实现 |
|---|---|---|---|
| `TpuTransferManager_New` | `0xeaba0a0` | 10 | `return operator new(1u);` — 一个 1 字节不透明占位句柄 |
| `TpuTransferManager_Free` | `0xeaba0c0` | 16 | `if (h) free(h);` — 释放占位符 |
```c
// TpuTransferManager_New sub_EABA0A0
void* TpuTransferManager_New(): return operator new(1) // dummy handle, no fields
// TpuTransferManager_Free sub_EABA0C0
void TpuTransferManager_Free(void* h): if (h) free(h)GOTCHA —
New返回的句柄不携带状态。实际传输机制是GetForPlatform单例,它在第一次方法调用时惰性解析并缓存在GetUnderlyingDeepseaPlatform::platform中。New/Free只存在于让主机端TpuTransferManagerC++ shim 有一个不透明this可传;不匹配的New/Free调用会泄漏/重复释放 1 字节,对设备状态无害,但属于主机侧分配器错误。
3. 主机 → 设备
目的
把主机 xla::Literal 移入设备内存(ShapedBuffer),并回答调用方在分配该设备缓冲区前需要的布局/大小问题。四者都通过 TransferManager vtable 弹跳调用。
函数映射
| 函数 | 地址 | 大小 | Vtable 槽位 | C++ 调用(解包后) |
|---|---|---|---|---|
TpuTransferManager_TransferLiteralToDeviceAsync | 0xeaba240 | 287 | +40 | m->TransferLiteralToDevice(stream, LiteralSlice(lit), shaped_buf, opts=0) |
TpuTransferManager_GetByteSizeRequirement | 0xeaba4c0 | 165 | +80 | m->GetByteSizeRequirement(host_shape) → int64 |
TpuTransferManager_HostShapeToDeviceShape | 0xeaba160 | 207 | +24 | m->HostShapeToDeviceShape(host_shape) → Shape(通过 ToC 输出) |
TpuTransferManager_ChooseCompactLayoutForShape | 0xeaba580 | 339 | +88 | m->ChooseCompactLayoutForShape(host_shape) → StatusOr<Shape> |
算法 — TransferLiteralToDeviceAsync
// TpuTransferManager_TransferLiteralToDeviceAsync sub_EABA240
// args: (a1=handle, a2=&SE_Stream, a3=XLA_Literal*, a4=XLA_ShapedBuffer*, a5=StatusRep** out)
ApiConverter::FromC(&lit, a3) // 24-byte MutableBorrowingLiteral
ApiConverter::FromC(&buf, a4) // 784-byte xla::ShapedBuffer
m = GetForPlatform(platform).value()
stream = *a2 // raw SE_Stream pointer, not converted
LiteralSlice slice(&lit) // wrap the borrowing literal as a slice
status = (*(*m + 40))(m, stream, &slice, &buf, /*opts*/0) // vtable +40
write_status_out(a5, status) // *a5 = status; Unref(old) unless aliased
~LiteralBase(&slice); ~ShapedBuffer(&buf); ~MutableBorrowingLiteral(&lit)
```text
`XLA_Literal` 作为 `MutableBorrowingLiteral` 到达(它借用调用方仍拥有的主机内存),并被重新包装为 `LiteralSlice` 传给传输调用;该传输只读主机字面量,从不写入。`SE_Stream` 原样传递(`*a2`),这是这个函数组唯一触碰 stream 句柄的地方;它是异步顺序令牌,传输会排入该 stream。
> **NOTE —** `GetByteSizeRequirement`(`+80`)和 `HostShapeToDeviceShape`(`+24`)是纯 shape→scalar / shape→shape 查询:它们只对主机 `Shape`(320 字节栈对象)执行 `FromC`,弹跳调用,并且(对 `HostShapeToDeviceShape`)用 `ToC` 把结果设备 `Shape` 写入 out-param。它们不分配设备内存,也不触碰 stream。`ChooseCompactLayoutForShape` 是第三个仅 shape 查询;它的 `+88` 槽位直接读自 `(*(*m + 88))(…)` 弹跳调用(out `StatusRep**` 为第一个参数,`StatusOr<Shape>` shape)。
---
## 4. 设备 → 主机
### 目的
把设备 `ShapedBuffer` 拉回主机字面量,并读取设备缓冲区携带的动态维度(用于动态形状程序)。两者都基于完成回调。
### 函数映射
| 函数 | 地址 | 大小 | Vtable 槽位 | C++ 调用(解包后) |
|---|---|---|---|---|
| `TpuTransferManager_TransferLiteralFromDevice` | `0xeaba360` | 352 | `+32` | `m->TransferLiteralFromDevice(stream, shaped_buf, MutableBorrowingLiteral, done_cb, opts=0)` |
| `TpuTransferManager_ReadDynamicShapes` | `0xeabb660` | 455 | `+48` | `m->ReadDynamicShapes(stream, shaped_buf, &out_shape)` |
### 算法 — `TransferLiteralFromDevice`
```c
// TpuTransferManager_TransferLiteralFromDevice sub_EABA360
// args: (a1, a2=&SE_Stream, a3=XLA_ShapedBuffer*, a4=XLA_Literal*, a5=StatusRep*, a6=callback ctx)
ApiConverter::FromC(&buf, a3) // 784-byte ShapedBuffer
ApiConverter::FromC(&lit, a4) // MutableBorrowingLiteral (host dest)
m = GetForPlatform(platform).value()
stream = *a2
copy_construct(&lit2, &lit); copy_construct(&lit3, &lit2) // 2 MBL copies for the closure
// build std::function<void(absl::Status)> closure capturing (a5=StatusRep out, a6=ctx)
done_cb = { __call_func = TpuTransferManager_TransferLiteralFromDevice::$_0,
policy = …::__create<$_0>() }
(*(*m + 32))(m, stream, &buf, &lit3, &done_cb, /*opts*/0) // vtable +32, async
if done_cb.policy.dtor: done_cb.policy.dtor(captured) // tear down closure
~MutableBorrowingLiteral(×3); ~ShapedBuffer(&buf)与主机→设备路径不同,这里会构造一个真正的 std::function<void(absl::Status)> 完成回调($_0 lambda + 一个 __policy_func thunk;两个 lambda thunk 都作为 _ZNSt3__u…TransferLiteralFromDeviceE3$_0E… 存活在符号表中)。异步设备读取完成时,回调会把最终状态写入调用方的 StatusRep* 槽位。两次 MutableBorrowingLiteral 拷贝是闭包按借用值捕获目标字面量,使其生命周期越过同步返回。
NOTE —
ReadDynamicShapes(+48)读取动态形状设备缓冲区的运行时已解析维度;它对ShapedBuffer执行FromC,弹跳调用,并用ToC输出一个Shape。槽位+48直接读自call *0x30(%rax)(*(*m + 48))弹跳调用;它紧挨在TransferLiteralToDevice(+40)之上,而不是像朴素名册顺序可能暗示的那样位于谓词之间。
5. Infeed / Outfeed(遗留 StreamExecutor 路径)
目的
流式主机↔设备通道:把主机字面量排入片上 infeed FIFO,从 outfeed FIFO 出队结果字面量。这些是进入队列子系统的遗留入口:它们通过 C-shim 和 TransferManager vtable 编组进 deepsea 驱动。现代 PJRT 原生路径(tpu::System::EnqueueInfeed)完全绕过这个函数组。队列机制、span 分块和阻塞语义见 Infeed / Outfeed Queues;本节只记录两个 C-ABI 行及其 vtable 弹跳。
函数映射
| 函数 | 地址 | 大小 | Vtable 槽位 | C++ 调用(解包后) |
|---|---|---|---|---|
TpuTransferManager_TransferLiteralToInfeed | 0xeabafa0 | 241 | +56 | m->TransferLiteralToInfeed(executor, LiteralSlice(lit)) |
TpuTransferManager_TransferBuffersToInfeed | 0xeabb0a0 | 638 | +136 | 预线性化设备缓冲区的 infeed |
TpuTransferManager_TransferLiteralFromOutfeed | 0xeabb320 | 260 | +64 | m->TransferLiteralFromOutfeed(executor, MutableBorrowingLiteral) |
TpuTransferManager_GetInfeedLayout | 0xf6a1a80 | 163 | (无 vtable — 见 §7) | TransferSizeUtil::ChooseGoodInfeedLayout(topology, shape) |
算法 — TransferLiteralToInfeed / FromOutfeed
// TpuTransferManager_TransferLiteralToInfeed sub_EABAFA0
// args: (a1, a2=&SE_StreamExecutor, a3=XLA_Literal*, a4=StatusRep** out)
ApiConverter::FromC(&lit, a3) // MutableBorrowingLiteral
m = GetForPlatform(platform).value()
ex = *a2 // the executor IS the queue selector here
LiteralSlice slice(&lit)
status = (*(*m + 56))(m, ex, &slice) // vtable +56 → driver EnqueueInfeed
write_status_out(a4, status); ~LiteralBase(&slice); ~MutableBorrowingLiteral(&lit)
// TpuTransferManager_TransferLiteralFromOutfeed sub_EABB320
// args: (a1, a2=&SE_StreamExecutor, a3=XLA_Shape*, a4=XLA_Literal*, a5=StatusRep** out)
ApiConverter::FromC(&shape, a3) // 320-byte Shape (the expected outfeed shape)
m = GetForPlatform(platform).value()
ex = *a2
ApiConverter::FromC(&lit, a4) // MutableBorrowingLiteral (host dest)
status = (*(*m + 64))(m, ex, &lit) // vtable +64 → driver DequeueOutfeed
write_status_out(a5, status); ~MutableBorrowingLiteral(&lit); ~Shape(&shape)
```text
两者都会把 `SE_StreamExecutor*`(`*a2`)传入 vtable 调用;这里 executor 指明*哪个设备的* infeed/outfeed 队列,这是该函数组中除了设备传输那一对之外 executor 的唯一作用。infeed 调用把字面量包装成只读 `LiteralSlice`;outfeed 调用传入可写 `MutableBorrowingLiteral` 作为结果。槽位 `+56` 落入 `DeepseaExecutor::EnqueueInfeed`,`+64` 落入 `DequeueOutfeed`(驱动叶子函数映射见 [Infeed / Outfeed page](../runtime/infeed-outfeed.md) 的 §legacy path)。
> **Note —** 两个不同地址支撑同一个 infeed/outfeed 调用,重新实现者不能混淆。[Infeed / Outfeed page](../runtime/infeed-outfeed.md) 把*主机侧 `TpuTransferManager::TransferLiteralToInfeed` C++ shim* 锚定在 `0xe9721c0`,把 `FromOutfeed` 锚定在 `0xe972660`,它们通过 `ExecutorApiFn()+560`/`+576` 抵达;这是**调用方**半边(转发进 `*ApiFn` 槽位的 SE shim)。本文记录的 `0xeabafa0`/`0xeabb320` 函数是**被调用方**半边,即槽位指向的 C-ABI 实现。由于 XLA 被静态链接,两半都存在于这个二进制中。
>
> **NOTE —** `TransferBuffersToInfeed`(`0xeabb0a0`,638 字节)是预线性化变体:它不接收字面量,而是接收已经是设备布局的缓冲区数组并将其排入队列,从而跳过线性化器。它通过 vtable 槽位 `+136`(`call *0x88(%rax)`)弹跳调用,这是一个比 `TransferLiteralToInfeed` 的 `+56` *更高且独立*的槽位,不是共享的 infeed 分支。缓冲区指针/长度数组参数(类似 `LinearizeToBuffers` 的输出)会在弹跳调用前循环执行 `FromC`。
---
## 6. 形状、布局与缓冲区访问谓词
### 目的
同步元数据侧:向设备缓冲区写入 tuple 索引表(让设备能找到每个 tuple 元素),以及两个询问缓冲区*现在*是否可在主机上读/写且无需设备同步的谓词。
### 函数映射
| 函数 | 地址 | 大小 | Vtable 槽位 | C++ 调用(解包后) |
|---|---|---|---|---|
| `TpuTransferManager_WriteSingleTupleIndexTable` | `0xeaba840` | 689 | `+120` | `m->WriteSingleTupleIndexTable(stream, device_addrs[], shape, &out_addr)` |
| `TpuTransferManager_CanShapedBufferBeAccessedNow` | `0xeaba6e0` | 174 | `+104` | `m->CanShapedBufferBeAccessedNow(executor, shaped_buf)` → `bool` |
| `TpuTransferManager_CanBufferBeAccessedNow` | `0xeaba7a0` | 141 | `+112` | `m->CanBufferBeAccessedNow(executor, device_addr)` → `bool` |
| `TpuTransferManager_PlatformId` | `0xeaba0e0` | 117 | `+16` | `m->PlatformId()` → `se::Platform::Id` |
| `TpuTransferManager_ResetDevices` | `0xeabb440` | 525 | `+72` | `m->ResetDevices(executors[])` |
### 算法 — 谓词和 `WriteSingleTupleIndexTable`
```c
// TpuTransferManager_CanBufferBeAccessedNow sub_EABA7A0
// args: (a1, a2=&SE_StreamExecutor, a3=SE_DeviceAddressBase*)
ApiConverter::FromC(&addr, a3) // 24-byte DeviceAddressBase
m = GetForPlatform(platform).value()
return (*(*m + 112))(m, *a2, &addr) // vtable +112 → bool
// TpuTransferManager_CanShapedBufferBeAccessedNow sub_EABA6E0
ApiConverter::FromC(&buf, a3) // 784-byte ShapedBuffer
m = GetForPlatform(platform).value()
r = (*(*m + 104))(m, *a2, &buf) // vtable +104 → bool
~ShapedBuffer(&buf); return r
// TpuTransferManager_WriteSingleTupleIndexTable sub_EABA840
// builds a heap vector<DeviceAddressBase> from the C address array (24 bytes each),
ApiConverter::FromC per element into operator new(24 * count) // FromC each region
ApiConverter::FromC(&shape, a_shape)
m = GetForPlatform(platform).value()
(*(*m + 120))(m, stream, addrs_vec, count, &shape, &out_c_addr, opts) // vtable +120PlatformId(+16)是最简单的 vtable 成员:不做编组,只解析并返回平台 id 整数。两个 CanBe...AccessedNow 谓词是 XLA 用来判断设备可见内存中的主机指针在不强制 stream 同步的情况下是否一致的廉价主机侧检查;两者都接收 SE_StreamExecutor* 加缓冲区/地址并返回 bool。WriteSingleTupleIndexTable 是最重的 vtable 成员(689 字节),因为它在 +120 弹跳调用前,会从 C 数组构造一个堆上的 std::vector<DeviceAddressBase>(每个元素 FromC 进 24 字节槽位)。
GOTCHA —
CanBufferBeAccessedNow(+112)接收单个SE_DeviceAddressBase(24 字节);CanShapedBufferBeAccessedNow(+104)接收完整XLA_ShapedBuffer(784 字节),调用后必须~ShapedBuffer。它们是相邻 vtable 槽位,但数值顺序与命名次序相反(shaped-buffer 变体是更低的偏移+104);假设命名↔偏移单调的重新实现者会接错表。ResetDevices(+72)接收 executor 数组;它的call *0x48(%rax)(*(*m + 72))弹跳调用直接读自反汇编。
7. 两个非 vtable 成员 — 直接线性化器调用
目的
LinearizeToBuffers 和 GetInfeedLayout 是该函数组中奇特的一对:它们不通过 xla::TransferManager vtable 分派。相反,它们直接解析 tpu::TpuTopology 并调用 xla::jellyfish 机制,也就是 PJRT 原生 infeed 路径使用的同一套线性化器 / 大小工具(见 Infeed / Outfeed Queues 的 §layout contract)。这是 C-shim 与现代运行时共享代码的地方。
函数映射
| 函数 | 地址 | 大小 | 直接被调方 |
|---|---|---|---|
TpuTransferManager_LinearizeToBuffers | 0xeabab00 | 594 | xla::jellyfish::LiteralLinearizer::LinearizeToBuffers(topology, …) |
TpuTransferManager_GetInfeedLayout | 0xf6a1a80 | 163 | xla::jellyfish::TransferSizeUtil::ChooseGoodInfeedLayout(topology, shape) |
算法
// TpuTransferManager_GetInfeedLayout sub_F6A1A80 (tpu_transfer_manager_c_api.cc:28)
// args: (a1=XLA_Shape* in, a2=XLA_Shape* out)
ApiConverter::FromC(&shape, a1)
topology = GetTopology(GetUnderlyingDeepseaPlatform::platform)
CHECK(topology != nullptr) // FATAL "topology != nullptr" if null
TransferSizeUtil::ChooseGoodInfeedLayout(&out_shape, topology, &shape)
ApiConverter::ToC(&out_shape, a2)
~Shape(&out_shape); ~Shape(&shape)
// TpuTransferManager_LinearizeToBuffers sub_EABAB00
// args: (…, a3=XLA_Literal*, a4=XLA_Shape*, a5/a6 = out buffer ptr/size arrays, a8=StatusRep**)
ApiConverter::FromC(&lit, a3)
ApiConverter::FromC(&shape, a4)
topology = platform->topology // *(*(platform+8)+184)
status = LiteralLinearizer::LinearizeToBuffers(topology, &lit, &shape, &out_vec,
InvokeObject<$_0…unique_ptr<uchar[]>>)
// hand the device-layout buffer vector back to C as two parallel new[] arrays:
*a6 = operator new(8 * count) // sizes
*a5 = operator new(8 * count) // pointers; each buffer memcpy'd into a fresh operator new
// (each entry copied from the linearizer's internal 56-byte-stride buffer descriptors)
```text
`GetInfeedLayout` 是纯 shape→layout helper(它的 FATAL-check 源字符串 `tpu_transfer_manager_c_api.cc:28` 锚定了该函数组的 TU)。`LinearizeToBuffers` 很重:它把主机字面量线性化成设备分 tile 的字节缓冲区,并把每个缓冲区复制进新 `operator new` 出来的块,返回两个平行的 `new[]` 数组(指针 + 大小),主机稍后必须用 `FreeBuffers` 释放。
### `FreeBuffers` — 匹配的释放器
```c
// TpuTransferManager_FreeBuffers sub_EABAF20
// args: (ptr_array, size_array, count)
void TpuTransferManager_FreeBuffers(void** ptrs, void* sizes, int64 count):
for i in 0..count: if ptrs[i]: free(ptrs[i]) // each buffer
free(ptrs) // the pointer array
if sizes: free(sizes) // the size array| 函数 | 地址 | 大小 | 角色 |
|---|---|---|---|
TpuTransferManager_FreeBuffers | 0xeabaf20 | 114 | 释放 LinearizeToBuffers 指针数组 + 每个缓冲区 + 大小数组 |
GOTCHA —
LinearizeToBuffers使用operator new分配(逐缓冲区 + 两个new[]数组),但FreeBuffers使用free()释放。在这个 glibc 构建中operator new转发到malloc,所以这对操作是平衡的;但如果重新实现者把operator new/operator delete[]接到与malloc/free不同的分配器,就会破坏堆。ABI 契约是“分配出的内容必须能由free()释放”。每次LinearizeToBuffers都要恰好配对一次FreeBuffers(ptrs, sizes, count)。
8. 完整 Vtable 槽位映射
重新实现者需要的一张表:每个 C 函数、它的地址,以及它弹跳经过的 xla::TransferManager vtable 字节偏移。每个偏移都直接读自反编译的 (*(*m + N))(…) 表达式,并与 call *0xNN(%rax) 反汇编交叉核对,因此所有行都是 CERTAIN。
| C 函数 | 地址 | Vtable 偏移 | C++ 方法(推断) |
|---|---|---|---|
New | 0xeaba0a0 | — | operator new(1) |
Free | 0xeaba0c0 | — | free |
PlatformId | 0xeaba0e0 | +16 | PlatformId() |
HostShapeToDeviceShape | 0xeaba160 | +24 | HostShapeToDeviceShape(Shape) |
TransferLiteralFromDevice | 0xeaba360 | +32 | TransferLiteralFromDevice(…, cb) |
TransferLiteralToDeviceAsync | 0xeaba240 | +40 | TransferLiteralToDevice(…) |
ReadDynamicShapes | 0xeabb660 | +48 | ReadDynamicShapes(…) |
TransferLiteralToInfeed | 0xeabafa0 | +56 | TransferLiteralToInfeed(exec, LiteralSlice) |
TransferLiteralFromOutfeed | 0xeabb320 | +64 | TransferLiteralFromOutfeed(exec, MBL) |
ResetDevices | 0xeabb440 | +72 | ResetDevices(executors) |
GetByteSizeRequirement | 0xeaba4c0 | +80 | GetByteSizeRequirement(Shape) |
ChooseCompactLayoutForShape | 0xeaba580 | +88 | ChooseCompactLayoutForShape(Shape) |
CanShapedBufferBeAccessedNow | 0xeaba6e0 | +104 | CanShapedBufferBeAccessedNow(exec, buf) |
CanBufferBeAccessedNow | 0xeaba7a0 | +112 | CanBufferBeAccessedNow(exec, addr) |
WriteSingleTupleIndexTable | 0xeaba840 | +120 | WriteSingleTupleIndexTable(…) |
TransferBuffersToInfeed | 0xeabb0a0 | +136 | 设备缓冲区的 infeed |
LinearizeToBuffers | 0xeabab00 | —(direct) | jellyfish::LiteralLinearizer::LinearizeToBuffers |
FreeBuffers | 0xeabaf20 | —(free) | n/a |
GetInfeedLayout | 0xf6a1a80 | —(direct) | jellyfish::TransferSizeUtil::ChooseGoodInfeedLayout |
QUIRK — vtable 偏移并不按名册顺序连续。C 函数按源码顺序发出(
New,Free,PlatformId, …),但它们的槽位跟随xla::TransferManager基类 vtable 布局(+16,+24,+32, …),其中交错着 C shim 不暴露的基类虚函数。构建 C++TransferManager子类的重新实现者必须复现基类 vtable 顺序,而不是 C 名册顺序,否则上面的每个槽位偏移都会错一个 frame。
相关组件
| 名称 | 关系 |
|---|---|
xla::TransferManager(TPU 子类) | 每个 vtable 后备行分派进入的 C++ 类 |
xla::TransferManager::GetForPlatform @ 0x1342f180 | 每个函数调用的每平台单例解析器 |
deepsea::executor::GetRegisteredDeepseaPlatform | 解析 TPU Platform,缓存在 GetUnderlyingDeepseaPlatform::platform 中 |
ApiConverter::ToC / FromC | 在边界两侧编组 XLA_Shape / XLA_Literal / XLA_ShapedBuffer / SE_DeviceAddressBase |
xla::jellyfish::LiteralLinearizer / TransferSizeUtil | LinearizeToBuffers / GetInfeedLayout 的直接被调方(无 vtable) |
ExecutorApiFn 表 | 其槽位指向这些 C 函数的函数指针结构 |
TpuExecutor_* 名册 | 形成对照的 shim:传递 SE_StreamExecutor* 句柄,而不是解析单例 |
交叉引用
- TfTpu C-API Shim — 本名册依赖的
*ApiFn访问器模型、不透明句柄约定和ApiConverter编组 - TpuExecutor 名册 — 形成对照的逐设备函数组,它确实把
SE_StreamExecutor*句柄穿线传过每次调用 - TpuProgram 名册 — 通过同一
ExecutorApiFn表抵达的兄弟序列化程序 C-ABI 函数组 - Infeed / Outfeed Queues —
TransferLiteral{ToInfeed,FromOutfeed}行作为遗留 StreamExecutor 入口进入的运行时队列机制 - PJRT Buffer & Memory — 设备侧
ShapedBuffer所属的 PJRT 缓冲区/内存 ABI - TfTpu_Initialize Bootstrap — 一次性填充指向这些函数的
ExecutorApiFn槽位