Skip to content

设备端压缩

本页中的所有地址、结构体偏移、vtable 槽位和魔数常量均适用于来自 libtpu-0.0.40-cp314 wheel 的 libtpu.so(build-id md5 89edbbe81c5b328a958fe628a9f2207d,build libtpu_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::Compact0x1e81c360,vtable 槽位 vt+0x90)。

描述“每个 buffer 位于 HBM 何处”的清晰架构是三分法:

  1. 编译期放置 — XLA 的 MSA + jellyfish ProgramMemoryAllocator 会把静态偏移冻结进程序(../compiler/msa-overview.md)。HBM 占用的大部分在编译期一次性布局,这就是运行时很少需要碎片整理的原因:打包问题在程序运行前已经解决。
  2. 运行时空闲链表数学(非移动)Allocate/Deallocate 用 best-fit 搜索 + 释放时积极双向合并服务动态表面(用户输入/输出 PJRT buffer、transfer staging、async-copy scratch、动态程序栈)。这记录在 hbm-allocator.md;它从不移动 live buffer。
  3. 运行时压缩(移动) — 当即使完全合并后的空闲空间也无法满足请求时,Compact 会把可移动 live buffer 向下重定位以整合空闲空间,并发出重定位计划std::vector<tpu::TpuMemmoves::Memmove>),由单独的 codegen 路径转换为设备端 DMA 程序。

因此本页是一份 present/absent 清单。PRESENT:完整的 live-buffer 重定位路径(CompactTpuMemmoves → 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 @ 0x1d12ed00CompactionRunner
设备驱动入口tpu::System::CompactMemory @ 0x1d0b6000(缩减程序栈 → EnqueueCompaction
PJRT 入口TpuClient::DefragmentMemory @ 0xf7fd660EnqueueDefragmentMemory @ 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 bufferCompaction(本页)是唯一会把 live buffer 字节移动到新 HBM 偏移的路径。两者互补:合并以低成本让空闲空间尽可能连续;当已合并的连续性仍不足时,压缩是重量级的最后手段。


存在什么,不存在什么

头条清单如下,每一行都在反编译中按字节验证(见 证据):

机制是否存在?证据
Live-buffer 重定位CompactPRESENTBestFitAllocator::Compact @ 0x1e81c360 发出 vector<TpuMemmoves::Memmove> 并重写 block map + free tree
与字节移动解耦的重定位计划PRESENTCompact 返回 move list;字节稍后由 Generate codegen 移动
OOM 触发的 defrag-and-retryPRESENTSystem::CompactMemory 0x1d0b6000TpuClient::DefragmentMemory 0xf7fd660ShouldRetryOnOom 0xf8141a0
保持静态/alias buffer 不可移动的 pinned setPRESENTCompact 接收 flat_hash_set<long> pinned;每个 block 进行 CRC32 SwissTable probe
压缩前的程序栈缩减PRESENTSystem::CompactMemory 调用 TpuProgramStack::MaybeShrink 0x1db0c100
重试之间释放 HBM 的程序驱逐PRESENTEvictLoadedPrograms 0xf80d2c0UnloadAllProgramsForCore(在 retry lambda 中)
Donation 驱动的复用(避免分配)PRESENTAllocateOutputBuffersWithInputReuse 0xf7ba9a0(见 buffer-donation-aliasing.md
Allocate 内部的重定位ABSENTAllocate 0x1e817820 没有 Compact/Memmove 引用;只有 SplitBlock
Deallocate 内部的重定位(释放时 defrag)ABSENTDeallocate 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::Compact0x1e81c360,vt+0x90)是分配器中唯一产生 buffer 重定位的方法。其签名(从二进制符号 demangle)是:

cpp
// 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)确认了五个阶段:

c
// 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 会异步入队到设备上。

c
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 是第二个状态级不可移动门。 独立于 pinned set,任何 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:

  1. TpuSharedMemoryCommonImpl::GenerateAndValidateCompactionPrograms(const TpuMemmoves&)0x1d130ec0)验证 move set(其 CHECK 字符串确认 compaction_buffer_base_ != nullptrcompaction_codegen() != nullptrmemmoves.type() == compaction_buffer().location().type()),并驱动 codegen。
  2. 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 地址翻译层可供重定向,因此字节会被复制。
  3. TpuSharedMemoryDriverCommonImpl::EnqueueCompactionImpl(const TpuMemmoves&, AnyInvocable<void(Status const&)>)0x1d12ed00)构造 CompactionRunnerCompactionRunner::Create)并把生成的程序入队到 core 的 command stream,DMA 完成时调用 completion callback。

stream-executor 表面也通过 TpuExecutor::EnqueueCompactionOnStreamForHbm0xe997400 / interface 0x1d0eff00)暴露同一能力,而 TpuNodeContext::CompactionSupported0xeaca440)决定某个 chip 是否支持它。

text
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::Compact0x1e81c360collect → sort → IntervalSet pack → emit TpuMemmoves → rewrite map/tree
TpuSharedMemory::EnqueueCompaction0x1d4bcde0每 core 压缩入口(TpuCompactionConfig + callback)
GenerateAndValidateCompactionPrograms0x1d130ec0验证 move set,驱动 codegen
TpuCompactionIsaEmitterCodegen::Generate0x1090ece0codegen 合并后的 VMEM↔HBM staged DMA transaction
EnqueueCompactionImpl0x1d12ed00构建 CompactionRunner,入队到 command stream
gtl::IntervalSet<long>::AddImpl0x1e824ae0在打包期间标记 interval occupied
gtl::IntervalSet<long>::IsDisjoint0x1cc99740测试候选放置窗口
__introsort(Compact LiveBlock0x1e81e260按 offset 排序 live block
TpuExecutor::EnqueueCompactionOnStreamForHbm0xe997400stream-executor 压缩表面
TpuNodeContext::CompactionSupported0xeaca440每 chip 能力门

触发压缩的内容:OOM 重试链

没有任何东西会主动运行 Compact。唯一驱动是分配失败。当 BestFitAllocator::Allocate0x1e817820)找不到合适的空闲块时,它返回 ResourceExhausted 诊断 "Attempting to allocate &lt;size&gt;. That was not possible. There are &lt;free&gt; free. The largest contiguous region of free memory is &lt;largest&gt; due to fragmentation."(site string 位于 best_fit_allocator.cc:129)。(表面相似的 "…at the bottom of memory…" 文案是由 ReserveBottomOfMemory @ 0x1e81b0c0 发出的不同字符串,不是此 Allocate OOM 路径发出的;见 hbm-allocator.md。)该错误向上传播经过四层分配器栈,并落入重试链:

c
// 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::AllocateTpuBufferWithRetry0xf7ec6a0)是 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 时碎片整理;二者可独立禁用。其确切 PjRtTpuClientConfig key 名未反向追踪(标在 open items 中)。megascale-aware 变体 CommonPjRtClient::ShouldRetryOnOom0xe6edc80)还会查询 DeviceAssignment,以便 pod slice 可以跨设备协调重试。

函数地址角色
BestFitAllocator::Allocate(OOM 位置)0x1e817820发出 ResourceExhausted "…due to fragmentation" 叶子错误
tpu::System::CompactMemory0x1d0b6000缩减程序栈 → EnqueueCompaction;bad core 时 MakeErrorImpl<13>
TpuProgramStack::MaybeShrink0x1db0c100压缩前回收 dynamic-stack HBM
TpuClient::DefragmentMemory0xf7fd660PJRT defrag 入口 → EnqueueDefragmentMemorySystem::CompactMemory
TpuClient::EnqueueDefragmentMemory0xf7fd180入队 defrag work item
TpuClient::ShouldRetryOnOom0xf8141a0≤ 2 次尝试;驱逐程序 + 碎片整理
CommonPjRtClient::ShouldRetryOnOom0xe6edc80megascale/pod-aware 重试协调
tfrt::tpu::AllocateTpuBufferWithRetry0xf7ec6a0async dependency-gated 重试;UnloadAllProgramsForCore
TpuExecutableLoadState::EvictLoadedPrograms0xf80d2c0在重试之间释放程序 HBM

为什么运行时压缩很少需要:MSA 故事

TPU 程序能够运行包含大量 kernel 的 745 MB 二进制并且几乎从不触发 Compact,原因是困难的打包问题在程序运行之前已经解决。XLA 的 MemorySpaceAssignment(MsaAlgorithm)加 jellyfish ProgramMemoryAllocator 会在编译期为每个静态 intra-program buffer 分配 memory space 和冻结的 byte offset,并序列化到已编译程序内的 ProgramMemoryMetadata。加载时,ProgramMemoryAllocator::CreateFromProto0x1c631f20)会重放这些 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 是另一个避免分配的机制。 在任何动态分配之前,AllocateOutputBuffersWithInputReuse0xf7ba9a0)会查询编译器发出的 HloInputOutputAliasConfig,并且对于 aliased output,原地复用 donated input buffer,不需要新的 HBM,也没有重定位。这是复用而不是压缩,但它同样会减少可能驱动碎片化的动态分配压力。见 buffer-donation-aliasing.md


合并 vs. 压缩:精确定义

为结束清单,下面并列列出两种空闲空间恢复机制。这是它们分成两个页面而不是一个页面的核心原因。

属性合并(hbm-allocator.md压缩(本页)
方法位于 Deallocate 0x1e819dc0 内部(通过 MergeBlock 0x1e819700Compact 0x1e81c360
何时每次释放,积极、立即仅 OOM 时,在合并已经无法帮助之后
是否移动 live 字节? — 只合并相邻空闲 — 通过 DMA 重定位可移动live buffer
机制扩展一个 map entry,擦除邻居 entry发出 TpuMemmoves 计划 → VMEM-staged DMA
是否触碰 live TpuBuffer从不是(可移动且未 pinned 的那些)
成本O(1) 摊销邻居查找 + O(log n) tree editO(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 &lt;X&gt; 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
  • 返回索引