设备端压缩
本页中的所有地址、结构体偏移、vtable 槽位和魔数常量均适用于来自
libtpu-0.0.40-cp314wheel 的libtpu.so(build-id md589edbbe81c5b328a958fe628a9f2207d,buildlibtpu_lts_20260413_b_RC00,clang trunk)。该镜像未 strip;demangled C++ 符号名均逐字引用。其他版本会有所不同。
摘要
本页用二进制证据精确回答一个问题:libtpu 运行时是否会通过重定位 live buffer 来执行 HBM 碎片整理,如果会,如何执行? 答案是会。这是一个不明显的结果,因为设备分配器(tpu::BestFitAllocator,见 hbm-allocator.md)是经典的非移动 boundary-tag 分配器,其 Allocate/Deallocate 路径只会合并相邻空闲块。live-buffer 重定位存在于一个单独的、由 OOM 触发的方法中:tpu::BestFitAllocator::Compact(0x1e81c360,vtable 槽位 vt+0x90)。
描述“每个 buffer 位于 HBM 何处”的清晰架构是三分法:
- 编译期放置 — XLA 的 MSA + jellyfish
ProgramMemoryAllocator会把静态偏移冻结进程序(../compiler/msa-overview.md)。HBM 占用的大部分在编译期一次性布局,这就是运行时很少需要碎片整理的原因:打包问题在程序运行前已经解决。 - 运行时空闲链表数学(非移动) —
Allocate/Deallocate用 best-fit 搜索 + 释放时积极双向合并服务动态表面(用户输入/输出 PJRT buffer、transfer staging、async-copy scratch、动态程序栈)。这记录在 hbm-allocator.md;它从不移动 live buffer。 - 运行时压缩(移动) — 当即使完全合并后的空闲空间也无法满足请求时,
Compact会把可移动 live buffer 向下重定位以整合空闲空间,并发出重定位计划(std::vector<tpu::TpuMemmoves::Memmove>),由单独的 codegen 路径转换为设备端 DMA 程序。
因此本页是一份 present/absent 清单。PRESENT:完整的 live-buffer 重定位路径(Compact → TpuMemmoves → DMA codegen → enqueue)、OOM 驱动的重试链、让 MSA 静态和被 alias 的 buffer 保持不可移动的 pinned-set,以及 donation 驱动的复用(分配的零拷贝替代)。ABSENT:Allocate/Deallocate 内部的任何重定位(它们只合并);任何周期性/后台碎片整理器;任何设备侧 arena/slab 压缩;任何 doubling 增长。本页负责的区别正是合并(每次释放时原地合并空闲邻居)vs. 压缩(只在 OOM 时重定位 live buffer)。
| 是否执行压缩? | YES — 重定位 live movable buffer 以整合空闲空间 |
| 触发条件 | 仅 OOM(分配失败 → 重试);从不周期性/后台执行 |
| 重定位引擎 | tpu::BestFitAllocator::Compact(flat_hash_set<long> pinned) @ 0x1e81c360 (vt+0x90) |
| 输出 | std::vector<tpu::TpuMemmoves::Memmove> — 重定位计划;字节由后续 codegen 步骤移动 |
| 字节移动 | TpuCompactionIsaEmitterCodegen::Generate @ 0x1090ece0(VMEM-staged DMA)→ EnqueueCompactionImpl @ 0x1d12ed00 → CompactionRunner |
| 设备驱动入口 | tpu::System::CompactMemory @ 0x1d0b6000(缩减程序栈 → EnqueueCompaction) |
| PJRT 入口 | TpuClient::DefragmentMemory @ 0xf7fd660 → EnqueueDefragmentMemory @ 0xf7fd180 |
| 重试策略 | TpuClient::ShouldRetryOnOom @ 0xf8141a0 — ≤ 2 次尝试;两次之间驱逐程序 + 碎片整理 |
| 不可移动桥接 | pinned absl::flat_hash_set<long> — MSA 静态和被 DMA/aliasing 持有的地址绝不移动 |
Allocate/Deallocate 中的重定位? | NO — 这些路径只合并空闲块(已验证不存在) |
| 算法 | 单遍、反向(高→低)greedy bin-pack,使用 gtl::IntervalSet<long> 占用 oracle |
| 源码标签 | learning/45eac/tpu/runtime/hal/internal/best_fit_allocator.cc(来自 ResourceExhaustedErrorBuilder site-string) |
| 置信度 | CONFIRMED(字节锚定,完整 Compact 函数体已反编译),除非某行或标注另有说明 |
范围和边界
本页负责present/absent 压缩清单、Compact 重定位方法,以及合并 vs. 压缩的区别。三个相邻关注点各有自己的页面;本页链接它们而不重复:
| 关注点 | 负责页面 |
|---|---|
| 非移动空闲链表:best-fit 搜索、双数据结构、释放时合并、拆分策略、对齐量子 | hbm-allocator.md |
| 编译期 HBM/VMEM 放置(静态布局,让大部分运行时压缩变得不必要) | ../compiler/msa-overview.md |
| Donation/aliasing 作为复用路径(donated input buffer 成为输出,完全避免分配) | buffer-donation-aliasing.md |
NOTE — 合并/压缩分割是关键点。 Coalescing(见 hbm-allocator.md)是原地的:每次
Deallocate时,通过扩展一个 map entry 并擦除另一个,把释放块与其相邻空闲邻居合并;不移动任何字节,也不触碰任何 live buffer。Compaction(本页)是唯一会把 live buffer 字节移动到新 HBM 偏移的路径。两者互补:合并以低成本让空闲空间尽可能连续;当已合并的连续性仍不足时,压缩是重量级的最后手段。
存在什么,不存在什么
头条清单如下,每一行都在反编译中按字节验证(见 证据):
| 机制 | 是否存在? | 证据 |
|---|---|---|
Live-buffer 重定位(Compact) | PRESENT | BestFitAllocator::Compact @ 0x1e81c360 发出 vector<TpuMemmoves::Memmove> 并重写 block map + free tree |
| 与字节移动解耦的重定位计划 | PRESENT | Compact 返回 move list;字节稍后由 Generate codegen 移动 |
| OOM 触发的 defrag-and-retry | PRESENT | System::CompactMemory 0x1d0b6000、TpuClient::DefragmentMemory 0xf7fd660、ShouldRetryOnOom 0xf8141a0 |
| 保持静态/alias buffer 不可移动的 pinned set | PRESENT | Compact 接收 flat_hash_set<long> pinned;每个 block 进行 CRC32 SwissTable probe |
| 压缩前的程序栈缩减 | PRESENT | System::CompactMemory 调用 TpuProgramStack::MaybeShrink 0x1db0c100 |
| 重试之间释放 HBM 的程序驱逐 | PRESENT | EvictLoadedPrograms 0xf80d2c0、UnloadAllProgramsForCore(在 retry lambda 中) |
| Donation 驱动的复用(避免分配) | PRESENT | AllocateOutputBuffersWithInputReuse 0xf7ba9a0(见 buffer-donation-aliasing.md) |
Allocate 内部的重定位 | ABSENT | Allocate 0x1e817820 没有 Compact/Memmove 引用;只有 SplitBlock |
Deallocate 内部的重定位(释放时 defrag) | ABSENT | Deallocate 0x1e819dc0 只调用 MergeBlock/free-tree 操作,即合并而非移动 |
| 周期性 / 后台碎片整理器 | ABSENT | 没有 timer/thread 驱动 Compact;唯一驱动是 OOM 重试路径 |
单次 Compact 调用内的多轮 fixpoint 压缩 | ABSENT | 单次反向扫描;未通过 IsDisjoint 的 block 留在原位 |
| 设备侧 arena/slab 压缩 | ABSENT | 每个 (core, tier) 一个固定区域 BestFitAllocator;没有 arena,见 hbm-allocator.md |
| 设备端 doubling 增长 | ABSENT | 区域是固定 [base, end);Expand/Shrink 只调整栈边界 |
WARNING — 这不是人们可能预期的否定结果。 朴素地读到“
BestFitAllocator是非移动 boundary-tag 分配器”,会得出 libtpu 无法碎片整理的结论。这个解读是错的。分配器的空闲链表方法确实从不重定位,但该分配器类还暴露了单独的Compact方法,后者会重定位,并且运行时会在 OOM 时调用它。诚实的总结是:常见情况非移动,耗尽时作为最后手段移动。
重定位方法:Compact
BestFitAllocator::Compact(0x1e81c360,vt+0x90)是分配器中唯一产生 buffer 重定位的方法。其签名(从二进制符号 demangle)是:
// 0x1e81c360 — returns the relocation plan by value (NRVO into a1).
std::vector<tpu::TpuMemmoves::Memmove>
tpu::BestFitAllocator::Compact(const absl::flat_hash_set<long>& pinned);它是带 pinned 排除集合的单遍、反向(高地址到低地址)greedy bin-pack。反编译函数体(约 ~3.9 kB)确认了五个阶段:
// Reconstructed from the 0x1e81c360 decompile (byte-confirmed structure).
std::vector<TpuMemmoves::Memmove>
Compact(const flat_hash_set<int64_t>& pinned) {
std::vector<TpuMemmoves::Memmove> moves; // RETURNED plan (begins empty)
// (1) COLLECT every allocated block from the boundary-tag map.
std::vector<LiveBlock> live; // LiveBlock = {begin,end,state} (48 B)
for (HashTableEntry& e : blocks_by_offset_) // walk SwissTable slots
if (e.state != kFree) // *((_DWORD*)slot+2) != 0
live.push_back({e.offset, e.offset + e.size, e.state});
// (2) SORT the live blocks (introsort, by offset).
__introsort(live.begin(), live.end()); // 0x1e81e260
// (3) REVERSE SWEEP: pack movable blocks against the top, against an occupancy oracle.
gtl::IntervalSet<int64_t> occupied; // absl btree of intervals
int64_t top = allocatable_range_end_; // this+0x70
std::vector<LiveBlock> kept;
for (LiveBlock& b : reverse(live)) { // high -> low
if (b.state == kReserved) { kept.push_back(b); continue; } // reserved -> immovable
int64_t addr = b.begin + base_offset_in_bytes_; // this+0x58
if (pinned.contains(addr)) { // CRC32 H2 SwissTable probe
kept.push_back(b);
occupied.Add({b.begin, b.end}); // 0x1e824ae0 (AddImpl)
continue; // PINNED -> never relocated
}
int64_t size = b.end - b.begin;
int64_t cand_lo = top - size, cand_hi = top; // candidate placement against the top
if (occupied.IsDisjoint({cand_lo, cand_hi})) { // 0x1cc99740 — placement is free
moves.push_back(Memmove{ b.begin / granule_, // +0 src, +8 dst, +0x10 size (GRANULE units)
cand_lo / granule_,
size / granule_ });
kept.push_back({cand_lo, cand_hi, b.state});
occupied.Add({cand_lo, cand_hi});
top = cand_lo;
} else {
kept.push_back(b); // overlap -> leave in place (single pass)
occupied.Add({b.begin, b.end});
}
}
// (4) REWRITE bookkeeping to the post-compaction layout, atomically in-call.
ClearBackingArray(blocks_by_offset_); // wipe the SwissTable
free_tree_.clear(); // __tree_deleter over the RB-tree
__introsort(kept.begin(), kept.end());
int64_t cursor = 0;
for (LiveBlock& b : kept) {
if (b.begin > cursor) { // gap below -> synthesize a free block
blocks_by_offset_.try_emplace(cursor, {cursor, kFree, b.begin - cursor});
free_tree_.insert(FreeBlock{cursor, b.begin}); // find_equal + balance_after_insert
}
blocks_by_offset_.try_emplace(b.begin, {b.begin, b.state, b.end - b.begin});
cursor = b.end;
}
if (cursor < allocatable_range_end_) { // trailing free block
blocks_by_offset_.try_emplace(cursor, {cursor, kFree, allocatable_range_end_ - cursor});
free_tree_.insert(FreeBlock{cursor, allocatable_range_end_});
}
// (5) RECOMPUTE the derived watermarks via a GetBlockIf probe at the reserved offset.
capacity_in_bytes_ = ...; // this+0xB8 updated from GetBlockIf result
return moves; // caller turns the plan into DMA programs
}为什么返回计划而不是执行
Compact 不会移动任何一个字节。它返回 std::vector<TpuMemmoves::Memmove>,即一组 {dst, src, size} 三元组,三个字段全都是 granule 单位(字节值除以 granule_in_bytes_,this+0x78)。字节移动由单独的流水线执行(下一节)。这种解耦很重要:分配器的 bookkeeping 会在 Compact 内部同步更新(所以 live tpu::TpuBuffer 句柄会立即解析到新偏移,因为它们是通过分配器当前 map 解析设备地址的),而物理 DMA 会异步入队到设备上。
struct tpu::TpuMemmoves::Memmove { // 24 B, GRANULE units
int64_t src; // +0 source offset / granule (the block's current begin)
int64_t dst; // +8 destination offset / granule (the new top placement)
int64_t size; // +0x10 size / granule
};
struct Compact::LiveBlock { // 48 B local helper
int64_t begin; // +0
int64_t end; // +8
int32_t state; // +0x10
// (trailing bytes used as a small-string/scratch slot in the decompile)
};pinned set 是 MSA / aliasing 不可移动性的桥
Compact 的唯一参数是 const absl::flat_hash_set<long>& pinned,即不得移动的 buffer 地址。在反编译的反向扫描中,每个候选 block 的绝对地址(b.begin + base_offset_in_bytes_)都会通过 CRC32-keyed Abseil SwissTable probe 在该集合中查找(_mm_crc32_u64 + vpcmpeqb group scan,行号确认 H2 metadata 匹配)。命中意味着该 block 被追加到 kept list,并且其 interval 被标记为 occupied;它绝不会被分配 Memmove。
这正是执行“MSA 拥有静态布局;运行时拥有剩余空间中的动态布局”的机制:
- MSA-static buffer(编译期放置,加载时由
ProgramMemoryAllocator::CreateFromProto重放,见 ../compiler/msa-overview.md)会传入pinned并保持原位。 - 存在未完成 DMA 或 aliasing hold 的 buffer(donated input 被复用为 output、处于传输中的 buffer,见 buffer-donation-aliasing.md)也会被 pinned,因为在 in-flight DMA 下方重定位字节会破坏它们。
- 只有剩余的动态、可移动 PJRT buffer 会收到
Memmove记录。
NOTE —
kReserved是第二个状态级不可移动门。 独立于pinnedset,任何state == kReserved的 block(例如由ReserveBottomOfMemory设置的内存底部保留区,见 hbm-allocator.md)都会被扫描完全跳过。因此,一个 buffer 如果是 reserved-state 或 address-pinned,就不可移动。
反向扫描打包的文字说明
Allocate 会把分配放在空闲块的顶部(top-down placement,见 hbm-allocator.md),因此 live buffer 天然聚集在高端。Compact 镜像这一点:它从高地址到低地址遍历 live block,并且对每个可移动 block,尝试把它尽可能高地放置(top - size),同时避开已经放置/固定的 interval 所组成的 IntervalSet。如果该候选窗口与所有 occupied 内容不相交,block 就重定位到那里并降低 top;否则 block 留在原位。净效果是可移动 buffer 向下滑动,填补 pinned buffer 留下的空隙,把空闲空间整合成底部的一段连续区间,而这正是后续 best-fit Allocate 可以满足的区间。
GOTCHA — 单遍,不是 fixpoint。 每次
Compact调用是一次反向扫描。未通过IsDisjoint测试的 block(因为 pinned block 位于其候选窗口中)会留在原位,并且不会在同一次调用中重试。如果出现多轮行为,它来自 OOM 重试循环再次调用整个 defrag 链(上限为 2 次尝试)。这已被观察到但未穷尽追踪,标记为 HIGH。
字节移动:从计划到设备端 DMA
Compact 产生计划;三个下游函数把它转为已执行的 DMA:
TpuSharedMemoryCommonImpl::GenerateAndValidateCompactionPrograms(const TpuMemmoves&)(0x1d130ec0)验证 move set(其CHECK字符串确认compaction_buffer_base_ != nullptr、compaction_codegen() != nullptr和memmoves.type() == compaction_buffer().location().type()),并驱动 codegen。TpuCompactionIsaEmitterCodegen::Generate(TpuSharedMemoryType, Span<TpuMemmoves::Memmove const>)(0x1090ece0)为实际 DMA 生成代码。反编译显示它读取Target::VmemSizeBytes/Target::VmemWordSizeBytes并构建vector<Transaction>,也就是在受 VMEM 容量限制的块中通过 VMEM staging move(HBM→VMEM→HBM),并把相邻 move 合并成 batched transaction。这就是为什么压缩是真正的物理重定位,而不是 remapping 技巧:没有 HBM 地址翻译层可供重定向,因此字节会被复制。TpuSharedMemoryDriverCommonImpl::EnqueueCompactionImpl(const TpuMemmoves&, AnyInvocable<void(Status const&)>)(0x1d12ed00)构造CompactionRunner(CompactionRunner::Create)并把生成的程序入队到 core 的 command stream,DMA 完成时调用 completion callback。
stream-executor 表面也通过 TpuExecutor::EnqueueCompactionOnStreamForHbm(0xe997400 / interface 0x1d0eff00)暴露同一能力,而 TpuNodeContext::CompactionSupported(0xeaca440)决定某个 chip 是否支持它。
BestFitAllocator::Compact(pinned) // build the plan + rewrite map/tree
│ std::vector<TpuMemmoves::Memmove>
▼
TpuSharedMemory::EnqueueCompaction (0x1d4bcde0) // per-core entry
▼
GenerateAndValidateCompactionPrograms (0x1d130ec0) // validate the move set
▼
TpuCompactionIsaEmitterCodegen::Generate (0x1090ece0) // codegen VMEM-staged DMA transactions
▼
EnqueueCompactionImpl (0x1d12ed00) -> CompactionRunner // enqueue on the device command stream| 函数 | 地址 | 角色 |
|---|---|---|
BestFitAllocator::Compact | 0x1e81c360 | collect → sort → IntervalSet pack → emit TpuMemmoves → rewrite map/tree |
TpuSharedMemory::EnqueueCompaction | 0x1d4bcde0 | 每 core 压缩入口(TpuCompactionConfig + callback) |
GenerateAndValidateCompactionPrograms | 0x1d130ec0 | 验证 move set,驱动 codegen |
TpuCompactionIsaEmitterCodegen::Generate | 0x1090ece0 | codegen 合并后的 VMEM↔HBM staged DMA transaction |
EnqueueCompactionImpl | 0x1d12ed00 | 构建 CompactionRunner,入队到 command stream |
gtl::IntervalSet<long>::AddImpl | 0x1e824ae0 | 在打包期间标记 interval occupied |
gtl::IntervalSet<long>::IsDisjoint | 0x1cc99740 | 测试候选放置窗口 |
__introsort(Compact LiveBlock) | 0x1e81e260 | 按 offset 排序 live block |
TpuExecutor::EnqueueCompactionOnStreamForHbm | 0xe997400 | stream-executor 压缩表面 |
TpuNodeContext::CompactionSupported | 0xeaca440 | 每 chip 能力门 |
触发压缩的内容:OOM 重试链
没有任何东西会主动运行 Compact。唯一驱动是分配失败。当 BestFitAllocator::Allocate(0x1e817820)找不到合适的空闲块时,它返回 ResourceExhausted 诊断 "Attempting to allocate <size>. That was not possible. There are <free> free. The largest contiguous region of free memory is <largest> due to fragmentation."(site string 位于 best_fit_allocator.cc:129)。(表面相似的 "…at the bottom of memory…" 文案是由 ReserveBottomOfMemory @ 0x1e81b0c0 发出的不同字符串,不是此 Allocate OOM 路径发出的;见 hbm-allocator.md。)该错误向上传播经过四层分配器栈,并落入重试链:
// tpu::System::CompactMemory (0x1d0b6000): the device-side defrag driver.
TpuEvent CompactMemory(const TpuSharedMemoryLocation& loc) {
auto* core = ResolveCore(loc);
if (!core) // best_fit_allocator path needs a core
return MakeError<ResourceExhausted>( // MakeErrorImpl<13>, system.cc:2410
"No attached TPU to compact.");
int seg = TpuSharedMemoryTypeToTpuSegmentMemoryType(loc.type()); // 0..2
for (TpuProgramStack* stack : core->program_stacks())
stack->MaybeShrink(stack->segment_limit(seg)); // 0x1db0c100 — reclaim dynamic-stack HBM first
return shm(loc)->EnqueueCompaction(/* -> BestFitAllocator::Compact(pinned) */);
}
// xla::TpuClient::ShouldRetryOnOom (0xf8141a0): the bound + recovery actions.
bool ShouldRetryOnOom(int attempt, PjRtDevice* dev, PjRtLoadedExecutable* exe, Status s) {
if (attempt > 1) return attempt < 2; // <= 2 total attempts
// tpu_pjrt_client.cc:4441 — verbatim LOG prefix
LOG(INFO) << "TpuLoadedExecutable::ExecutePrepareWithOomRetries "
"attempting to defragment and retry after seeing error: " << s;
if (this->evict_programs_on_oom_ /*+0x67*/)
for (auto& [id, ls] : loaded_executables_)
if (ls != exe) ls->EvictLoadedPrograms(); // 0xf80d2c0 — free program HBM
if (this->defragment_on_oom_ /*+0x69*/)
DefragmentMemory(dev->core()->LocalSharedMemory(kHbm)); // 0xf7fd660 -> CompactMemory
return attempt < 2;
}异步叶子 tfrt::tpu::AllocateTpuBufferWithRetry(0xf7ec6a0)是 dependency-gated,而不是 spin-loop:如果 System AsyncValueRef 仍 pending,它会入队一个 waiter,携带重试 continuation($_0 @ 0xf7ed620),后者也会调用 TpuCompilationCache::UnloadAllProgramsForCore 来释放程序 HBM,然后在 dependency resolved 后重新运行 System::Allocate。因此 OOM 后的恢复序列依次是:(a) 缩减动态程序栈;(b) 驱逐/卸载 loaded program 以回收其静态 HBM;(c) 运行 Compact 来重定位可移动 buffer 并整合剩余空间;(d) 重试分配,最多两次。
NOTE — 两个 config bool 控制恢复。
TpuClient+0x67切换程序驱逐,TpuClient+0x69切换 OOM 时碎片整理;二者可独立禁用。其确切PjRtTpuClientConfigkey 名未反向追踪(标在 open items 中)。megascale-aware 变体CommonPjRtClient::ShouldRetryOnOom(0xe6edc80)还会查询DeviceAssignment,以便 pod slice 可以跨设备协调重试。
| 函数 | 地址 | 角色 |
|---|---|---|
BestFitAllocator::Allocate(OOM 位置) | 0x1e817820 | 发出 ResourceExhausted "…due to fragmentation" 叶子错误 |
tpu::System::CompactMemory | 0x1d0b6000 | 缩减程序栈 → EnqueueCompaction;bad core 时 MakeErrorImpl<13> |
TpuProgramStack::MaybeShrink | 0x1db0c100 | 压缩前回收 dynamic-stack HBM |
TpuClient::DefragmentMemory | 0xf7fd660 | PJRT defrag 入口 → EnqueueDefragmentMemory → System::CompactMemory |
TpuClient::EnqueueDefragmentMemory | 0xf7fd180 | 入队 defrag work item |
TpuClient::ShouldRetryOnOom | 0xf8141a0 | ≤ 2 次尝试;驱逐程序 + 碎片整理 |
CommonPjRtClient::ShouldRetryOnOom | 0xe6edc80 | megascale/pod-aware 重试协调 |
tfrt::tpu::AllocateTpuBufferWithRetry | 0xf7ec6a0 | async dependency-gated 重试;UnloadAllProgramsForCore |
TpuExecutableLoadState::EvictLoadedPrograms | 0xf80d2c0 | 在重试之间释放程序 HBM |
为什么运行时压缩很少需要:MSA 故事
TPU 程序能够运行包含大量 kernel 的 745 MB 二进制并且几乎从不触发 Compact,原因是困难的打包问题在程序运行之前已经解决。XLA 的 MemorySpaceAssignment(MsaAlgorithm)加 jellyfish ProgramMemoryAllocator 会在编译期为每个静态 intra-program buffer 分配 memory space 和冻结的 byte offset,并序列化到已编译程序内的 ProgramMemoryMetadata。加载时,ProgramMemoryAllocator::CreateFromProto(0x1c631f20)会重放这些 offset,而运行时 BestFitAllocator 会被告知每个静态 buffer 应该放在哪里;它不会为这些 buffer 运行 best-fit 搜索,也永远不需要重定位它们(它们是 pinned)。见 ../compiler/msa-overview.md。
这对重新实现者是关键架构洞察:编译期打包是主要的抗碎片策略;运行时压缩是安全网。 运行时空闲链表数学(以及任何压缩)只针对 MSA 静态布局之后剩余 HBM 中的动态表面执行:用户输入/输出 PJRT buffer、transfer staging、async-copy scratch、动态程序栈。因为相对于静态程序 footprint,这些通常更大、更少且生命周期更短,运行时大多依靠 best-fit + coalescing 就足够,只有在真正压力下才触发 Compact。
NOTE — donation 是另一个避免分配的机制。 在任何动态分配之前,
AllocateOutputBuffersWithInputReuse(0xf7ba9a0)会查询编译器发出的HloInputOutputAliasConfig,并且对于 aliased output,原地复用 donated input buffer,不需要新的 HBM,也没有重定位。这是复用而不是压缩,但它同样会减少可能驱动碎片化的动态分配压力。见 buffer-donation-aliasing.md。
合并 vs. 压缩:精确定义
为结束清单,下面并列列出两种空闲空间恢复机制。这是它们分成两个页面而不是一个页面的核心原因。
| 属性 | 合并(hbm-allocator.md) | 压缩(本页) |
|---|---|---|
| 方法 | 位于 Deallocate 0x1e819dc0 内部(通过 MergeBlock 0x1e819700) | Compact 0x1e81c360 |
| 何时 | 每次释放,积极、立即 | 仅 OOM 时,在合并已经无法帮助之后 |
| 是否移动 live 字节? | 否 — 只合并相邻空闲块 | 是 — 通过 DMA 重定位可移动live buffer |
| 机制 | 扩展一个 map entry,擦除邻居 entry | 发出 TpuMemmoves 计划 → VMEM-staged DMA |
是否触碰 live TpuBuffer? | 从不 | 是(可移动且未 pinned 的那些) |
| 成本 | O(1) 摊销邻居查找 + O(log n) tree edit | O(n log n) collect+sort + interval-set pack + DMA + 完整 map/tree rebuild |
| 恢复内容 | 由邻接关系导致的空闲碎片 | 非相邻空闲碎片(外部碎片) |
| 解决的失败模式 | 已释放块旁边的小空闲间隙 | "largest contiguous region … due to fragmentation" OOM |
合并保证 free tree 永远不会持有两个物理相邻的空闲块(见 hbm-allocator.md);这让 best-fit 保持诚实,并免费恢复所有可由邻接恢复的空间。合并不能做的是合并被 live buffer 隔开的空闲空间;这正是压缩所填补的缺口,它会把中间的 live buffer 移开。OOM 诊断中的 "largest contiguous region of free memory is <X> due to fragmentation" 正是这种情况的症状:总空闲字节数足够,但没有单个连续区间足够,因为 live buffer 位于间隙之间。Compact 把可移动的那些滑到一边,从而把这些间隙合并成一个区间。
交叉引用
- hbm-allocator.md — 非移动空闲链表:best-fit 搜索、双数据结构、释放时合并(本页重定位的原地对应物)、拆分策略、对齐量子
- ../compiler/msa-overview.md — 冻结静态偏移的编译期放置 pass,也是运行时压缩很少需要的主要原因
- buffer-donation-aliasing.md — donation/aliasing 作为零拷贝复用路径,以及让 buffer 在压缩期间 pinned 的 DMA/aliasing hold 来源
- tpu-buffer-layout.md — 设备缓冲区的 HBM 偏移如何映射到设备端 tile 布局(
Compact实际重定位的字节) - overview.md — 五个片上内存 tier,以及 HBM 分配器在映射中的位置
- hbm-dma-alignment.md — 让每个重定位偏移保持 DMA 合法的 1024 B DMA 量子
- vmem-allocator.md, cmem-pool.md — 使用同一
BestFitAllocator类(因而同样有Compact)的同级 tier - 返回索引