全部术语

GLOSSARY ENTRY

KV Offload 与 Prefill/Decode Disaggregation

  • KV Offload
  • KV Swap
  • Prefill/Decode Disaggregation
  • P/D Disaggregation

KV Offload 在 GPU 与 CPU/远端层级间迁移请求状态以换取 GPU 容量,P/D Disaggregation 则让 prefill 与 decode 由不同 workers 承担并交接 KV;两者把显存压力转成传输、排队与一致性成本。

两种机制的共同对象是 KV,优化边界不同

  • KV Offload/Swap:同一请求暂时不在 GPU 执行时,把 KV Cache从 GPU 搬到 CPU pinned/pageable memory、其他 GPU 或远端存储;恢复前再搬回。
  • Prefill/Decode Disaggregation(P/D):prefill worker 处理 prompt 并产生 KV,然后把状态交给独立 decode worker,二者可针对 compute-heavy prefill 与 latency/bandwidth-heavy decode 分别配置。

Offload 主要是容量/抢占策略;P/D 主要是服务架构/资源池分离。P/D handoff 本身是一种 KV transfer,可能使用 RDMA、GPU P2P、host staging 或 cache connector;但“用了 P/D”不等于请求发生过 preemption/offload。

KV ownership 状态机

一个请求的 cache ownership 不应同时由两个可写 owner 模糊持有:

GPU-resident
  ├─ offload begin → transferring-out → CPU/remote-resident
  │                                      └─ reload → transferring-in → GPU-resident
  └─ P/D handoff → prefill-owner freezes snapshot
                       → transfer + manifest
                       → decode-owner validates/installs
                       → handoff committed

Handoff manifest 至少需要:request/model/adapter identity、token ids/length、每层 cache fields、dtype/quantization、block size/layout、logical positions/RoPE config、source/target TP/CP layouts 与 checksums/version。仅传一组裸 tensors,decode worker 无法判断它们是否与本地模型和 page table 兼容。

如果 source/target 并行度不同,还需重分片:例如 prefill TP=4、decode TP=2 时,K/V head/context shards 的所有权必须转换,而不是把 rank 0 buffer 原样发给 decode rank 0。

Payload 与带宽下界

标准 cache bytes:

MKV=2LBTHkvDsM_{KV}=2LBTH_{kv}Ds

本页实验取 L=16,B=1,T=1024,Hkv=8,D=128,s=2L=16,B=1,T=1024,H_{kv}=8,D=128,s=2,得到 64 MiB。仅看传输 payload,带宽 BWBW 的理论下界:

TcopyMKVBWT_{copy}\ge\frac{M_{KV}}{BW}

在 nominal 100 Gbit/s(12.5 GB/s)网络上,64 MiB payload-only 下界约 5.369 ms。这不含 serialization、registration、protocol header、NIC/PCIe hops、queue、同步、block-table metadata、retries 或 target install;也未扣除编码效率,不能称作实测网络延迟。

若 offload 后要恢复,至少传两次:

TroundtripTD2H+Tqueue/residency+TH2DT_{roundtrip}\gtrsim T_{D2H}+T_{queue/residency}+T_{H2D}

而 recompute preemption 的成本是释放后未来重新 prefill。选择 swap 还是 recompute,应比较请求长度、预计暂停时间、传输/compute 资源争用和 SLO,不存在全局最优。

可运行 pinned CPU round-trip

实验问题:64 MiB FP16 KV tensor 在本机 GPU→pinned CPU→GPU 能否逐元素保持一致?同步 CUDA Event 计时得到什么本机 copy 结果?同 payload 在 nominal 100 Gbit/s 上的纯 bytes 下界是多少?

import platform,statistics,torch

torch.manual_seed(233)
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")

def median_copy_ms(fn,warmup=5,rounds=20):
    for _ in range(warmup): fn()
    torch.cuda.synchronize(); samples=[]
    for _ in range(rounds):
        start=torch.cuda.Event(enable_timing=True); end=torch.cuda.Event(enable_timing=True)
        start.record(); fn(); end.record(); end.synchronize()
        samples.append(start.elapsed_time(end))
    return statistics.median(samples)

def main():
    layers,kv,heads,tokens,dim=16,2,8,1024,128; dtype=torch.float16
    shape=(layers,kv,heads,tokens,dim)
    payload=layers*kv*heads*tokens*dim*torch.empty((),dtype=dtype).element_size()
    if DEVICE.type=="cuda":
        gpu=torch.randn(shape,device=DEVICE,dtype=dtype)
        cpu=torch.empty(shape,device="cpu",dtype=dtype,pin_memory=True)
        restored=torch.empty_like(gpu)
        offload=lambda:cpu.copy_(gpu,non_blocking=True)
        reload=lambda:restored.copy_(cpu,non_blocking=True)
        offload(); torch.cuda.synchronize(); reload(); torch.cuda.synchronize()
        torch.testing.assert_close(restored,gpu,rtol=0,atol=0)
        d2h=median_copy_ms(offload); h2d=median_copy_ms(reload)
        d2h_bw=payload/2**30/(d2h/1e3); h2d_bw=payload/2**30/(h2d/1e3)
    else:
        gpu=torch.randn(shape,dtype=dtype); cpu=gpu.clone(); restored=cpu.clone()
        torch.testing.assert_close(restored,gpu,rtol=0,atol=0)
        d2h=h2d=d2h_bw=h2d_bw=float("nan")
    nominal_gbps=100; lower_ms=payload/(nominal_gbps*1e9/8)*1e3
    assert payload==64*2**20 and abs(lower_ms-5.36870912)<1e-6
    name=torch.cuda.get_device_name(0) if DEVICE.type=="cuda" else "CPU"
    print(f"python={platform.python_version()} torch={torch.__version__}")
    print(f"device={DEVICE} name={name} dtype={dtype} KV={shape} payload_MiB={payload/2**20:.3f}")
    print(f"roundtrip_max_abs_diff={(restored.cpu()-gpu.cpu()).abs().max():.3e}")
    if DEVICE.type=="cuda":
        print(f"pinned_D2H_median_ms={d2h:.3f} effective_GiB_s={d2h_bw:.3f}")
        print(f"pinned_H2D_median_ms={h2d:.3f} effective_GiB_s={h2d_bw:.3f}")
    print(f"PD_nominal_network_Gbps={nominal_gbps} payload_only_lower_bound_ms={lower_ms:.3f}")

if __name__=="__main__":
    with torch.inference_mode(): main()

完整文件位于 examples/kv_offload_pd.py。2026-08-13 在 RTX 3080 上,round-trip 最大差 0;pinned D2H/H2D 中位数约 9.924/9.998 ms,有效 6.298/6.251 GiB/s。这是当前单机、同步、一个 tensor 的 PCIe/host path 结果,不代表其他主板、NUMA、并发 copy 或 GPUDirect/RDMA。

脚本没有验证 P/D 网络性能;100 Gbit/s 只是公式代入。生产 benchmark 必须用实际 connector、并发请求、KV block 粒度、校验/install 和 source/target worker queue 测 TTFT/TPOT。

P/D 为什么可能有效,也可能更差

可能收益:

  • prefill GPU 针对大 GEMM/长 sequence batch,decode GPU 针对小步低延迟/CUDA Graph;
  • 长 prompt 不再直接干扰 decode iterations;
  • 两个资源池可独立扩缩、admission 和 utilization。

新增成本:

  • prefill 完成后必须传全部必需 KV,首 token/下一 decode 可能等待 handoff;
  • prefill/decode pool 负载不平衡会在中间形成 queue;
  • KV layout/quantization/parallel mesh conversion;
  • failure/retry/duplicate ownership、source release 时机;
  • tokenizer/model/adapter/version 必须一致;
  • network/NIC/PCIe 与 MoE/collectives争用。

P/D 的核心指标至少包括 prefill queue、prefill time、KV transfer queue/bytes/time、decode admission、TTFT/TPOT、handoff failures 和 end-to-end goodput。只比较 GPU utilization 不能证明服务收益。

与调度和内存层级的组合

  • Scheduler/Admission决定何时 offload、recompute、reject,以及 P/D 两个 pool 的预算。
  • Chunked Prefill/Preemption提供 recompute/swap 语义;本页加入实际 bytes 与 ownership transfer。
  • PagedAttention让 offload/handoff 按 blocks 而非整个 contiguous sequence 操作,但 page-table format 需跨 worker 兼容或重建。
  • 硬件拓扑决定 GPU↔CPU↔NIC/GPU 路径;pinned memory 绑错 NUMA node 会降低 copy。
  • Prefix Cache若目标 worker已拥有相同 immutable prefix blocks,可只传 missing suffix,但 identity/refcount 协议必须严格。

常见失败:source 在 target install 前释放 KV;同一请求 source/target 都继续写;partial transfer 被当完整 cache;position/adapter/quantization 不匹配;取消请求未中断/丢弃在途 transfer;恢复 copy 占用关键 stream,反而阻塞 decode。

参考资料