Engram 预取与 NUMA 放置:从共享页到关键路径
针对双路 x86 与 PCIe GPU,追踪 Engram 共享表的页放置、UVA 查表和串行预取,建立可验证的性能实验
本文目录
同机 DP 共享把 Engram 大表的物理容量降下来之后,下一个问题是:共享页离哪张 GPU 更近,查表结果能否赶在消费层之前就绪? 本页承接 TP 分片与同机 DP 共享,专门研究双路 x86 + PCIe GPU 的放置与预取。
固定 vLLM 0.31.0 db9527a46873454610df6dbedf79a36d6bf1a7f6 的 NVIDIA 实现,Linux 内存策略对照 v6.12 文档与 mm/shmem.c。当前云环境只有 NUMA node 0,没有 NVIDIA 设备;本页完成源码核验、成本推导和实验设计,尚无跨 NUMA / GPU 性能结果。Grace 的 NVLink-C2C 路径需要单独验证。
- 一句话:先确认共享物理页放在哪里,再评估预取隐藏的等待是否超过它对主计算造成的干扰。
- 三个判断:node-local DP 不等于 socket-local;CPU affinity 不等于内存页放置;background grid 的上限不等于保留了一半 SM。
- 核心模型:串行预取满足 、;消费时暴露等待为 ,端到端收益还要扣除计算与通信的回归。
- 边界:stock 代码没有按 socket 复制共享表或关闭预取的独立开关;这些实验对照需要研究补丁,不能用改变 CPU offload 的方式冒充同条件预取对照。
1. 实际路径:GPU kernel 直接读取 host rows
DPSharedEngramStorage 在 /dev/shm 建立一份 MAP_SHARED backing,各进程注册自己的虚拟地址,再通过 UVA 获取 accelerator view。cuda_view.cu 对已 pinned 的 tensor 调用 cudaHostGetDevicePointer,并保留原 CPU tensor 的生命周期;若未 pinned,它会另建 pinned buffer 并复制,这也是共享存储必须检查 tensor.is_pinned() 的原因。
随后 _engram_lookup_kernel 用 tl.load 读取 host FP8 rows 与 ue8m0 scales,做反量化,并将 BF16 写入本 rank 的 GPU staged_rows。offload 预取是 GPU lookup kernel,不是先把整张表异步拷进 HBM。只统计 CUDA memcpy 事件会漏掉这条访问路径。
flowchart TB
subgraph S0[Socket 0]
M0[NUMA 0:共享表物理页]
R0[PCIe root 0]
G0[GPU 0:lookup → BF16 staging]
M0 --> R0 --> G0
end
subgraph S1[Socket 1]
R1[PCIe root 1]
G1[GPU 1:lookup → BF16 staging]
R1 --> G1
end
M0 --> X[跨 socket 互联] --> R1
若两个 socket 的 GPUs 都访问同一 shard 的随机 rows,一份物理页无法同时成为双方的本地主存。NVLink 连接 GPU 也不会自动把这些 host pages 复制到另一 socket;应分别检查 GPU 间通信和 GPU→host 路径。
2. 页放置的决定点早于权重加载
2.1 leader 写权重不等于 leader 首次分配页
共享分配的源码顺序是:leader 创建并 truncate 文件 → 广播路径 → 所有 peers 映射并 cudaHostRegister → 映射确认 → leader 加载 checkpoint → 权重 barrier。
truncate 设置长度,不证明所有物理页已分配;共享路径也没有在注册前由 leader 显式逐页写入。注册发生在权重加载前,因此不能从“只有 leader 写 checkpoint”推出“所有页都 first-touch 在 leader 的 NUMA 节点”。实际注册是否触发缺页、由哪一线程执行分配,以及 peers 并发注册是否影响分布,都要按具体驱动与内核测量。
Linux v6.12 的 shmem_get_pgoff_policy() 优先取共享对象的 policy,否则使用当前 task policy;创建 inode 时还会从 tmpfs mount policy 初始化共享 policy。这里 /dev/shm 的 filesystem 与 mount options、进程/线程的 memory policy、cpuset 允许的 nodes 都参与决定结果。不要把普通磁盘文件的 MAP_SHARED 规则直接套到 tmpfs。
对照之下,私有 THP 路径明确执行 MADV_HUGEPAGE,再由 owner[::PAGESIZE] = 0 触页,之后才注册。它有更明确的预触页阶段,但仍须确认实际 memory policy 和 huge-page 覆盖率。
2.2 vLLM 绑核选项与实际策略
该版本 ParallelConfig.numa_bind 默认关闭。启用 --numa-bind 后,worker wrapper 尝试使用 --cpunodebind 或 --physcpubind,同时使用 --membind。--numa-bind-nodes 按实际 GPU assignment 提供 node 列表;例如 [0,0,1,1] 只适用于已确认相应四卡映射的机器。
_resolve_numactl_args() 会探测是否可执行:完整参数失败时,可能去掉 --membind,保留 CPU binding;还可能继续退化为无 binding。自动检测遇到既有 CPU affinity 或受限 memory-policy 权限,也可能不可用。
这意味着 --numa-bind 只是请求,实验记录必须包含实际 worker 的 CPU mask、memory policy 与页分布。重新绑 CPU 不会自动搬迁已经分配的页;对已注册的长期 host storage,也不能假定自动 NUMA balancing 会完成迁移。放置对照优先从新分配、注册前的控制开始。
2.3 最小现场检查
以下是只读采集命令;ENGRAM_PID 应填实际持表 worker 的 PID,各 worker 都应采集。
nvidia-smi topo -m
nvidia-smi --query-gpu=index,uuid,pci.bus_id --format=csv
lscpu -e=CPU,NODE,SOCKET
numactl --hardware
findmnt -T /dev/shm -o TARGET,FSTYPE,OPTIONS
ENGRAM_PID=12345
rg 'Cpus_allowed_list|Mems_allowed_list' /proc/$ENGRAM_PID/status
numastat -p "$ENGRAM_PID"
cat /proc/$ENGRAM_PID/numa_maps
cat /proc/$ENGRAM_PID/smaps_rollup
Mems_allowed_list 是允许范围,不是已经实施的 membind 策略。numastat -p 是进程总览,应在研究补丁中打印每个表的 data_ptr、bytes、head range、TP/EDP group 与 backing 身份,用地址范围关联 /proc/<pid>/maps、numa_maps 和 smaps。文件已 unlink 时,映射仍可存活,不能靠路径是否存在判断共享失败。
记录三个时间点:注册结束、checkpoint 加载结束、稳态回放之后。容量用节点物理占用及 PSS 交叉验证,避免多个 peers 的 RSS 重复计数;页归属用每表的 N0/N1 分布验证。需要更精细时使用可用的页查询工具,并记录权限和不可查询的页,不能把失败结果当作 node 0。
3. 三种放置方案及其代价
这里的 指完整 Engram 表的逻辑 bytes;各 TP shard 的放置仍需单独记录。
| 对照 | 理想节点表容量 | 预期作用与待验证风险 | 实现条件 |
|---|---|---|---|
| 单份表集中在 node 0 或 node 1 | 一侧访问更近,另一侧可能增加跨 socket 流量;两侧争同一 memory controller | 注册前控制每个 backing 的 policy / 预触页,并验证分布 | |
| 单份表跨 nodes 交错 | 可能分散 controller 负载,但每侧仍有远端访问,延迟与 tail 未必更好 | 注册前设置受控共享 policy / 分段触页;验证实际比例 | |
| 每 socket 一份表,同 socket DP peers 共享 | 约 | 用容量换双方的 host locality,仍受本 socket PCIe / DRAM 争用限制 | 研究性调整共享 group/backing;保持 TP shard 与输出合同 |
默认 EDP group 的节点边界来自并行 rank placement,没有按 NUMA socket 划分共享组。若同一 TP shard 的 DP peers 分布在两个 socket,仅给各 worker 绑自己的 GPU-local node,不能让同一共享页同时满足两种 locality。
约 的对照假设两 socket 各需要一份完整表。若某个 TP head shard 的所有 consumers 本来就在同一 socket,优先将这一份 shard 放在该 socket,可能保持总容量约 就满足 locality。安排 rank placement 时还要核对 TP/EP 通信,避免改善 host 查表却恶化 GPU collective 路径。
每 socket 一份表是候选设计,不是 stock flag 保证的功能。它要求容量足够、组内 replica/head 范围一致,并重新验证加载 barrier、生命周期和查询正确性。也不能把 dp_shared_memory=False 简单当成独立副本基线:当前 NVIDIA 构造还会读取有效 EDP size,可能得到跨 DP 分片,增加 hash/rows collectives。
不要为单个实验修改整机 /dev/shm 的 mount policy。优先隔离运行或对研究分配建立显式 policy;保留同样的 IPC 可见性,并确认 namespace、cpuset、容量没有随放置方案一起改变。
4. 预取收益取决于串行队列和消费期限
模型在进入本地 decoder layers 前计算 hash IDs,并按层顺序调用 prepare_embeddings()。本地 Engram layers 共用一条 stream,串行执行 lookup,避免多个 offload kernels 一起争抢计算资源。
每次 _start_prefetch() 等待当前主 stream 的依赖,保留 hash storage 生命周期,提交 lookup 并记录该层 event;消费层只 wait_event 自己的 event。@eager_break_during_capture 允许 lookup 跨 piecewise graph segments。这里不能把 stream wait 一概解释为 host 阻塞。
设 为第 个 lookup 的输入及 stream 依赖就绪时刻, 为其实际执行时间, 为预取 stream 初始可用时刻, 为主 stream 到达该层等待点的时刻:
这是解释已观测时间线的模型,不是独立固定参数的性能预测:前层等待会推迟后层 ,lookup 争用也会改变主计算和 。后层虽然有更多 lead time,也可能在预取 FIFO 中排队更久;必须同时看排队时间 、kernel 时间 和暴露等待 。
common/engram.py::lookup() 的 persistent grid 为:
rows = tokens × local_heads
tiles = ceil(rows / 16)
grid = min(tiles, SM_count // 2) # background=True
这限制的是 Triton programs 数,不是硬性预留半数 SM。实际 occupancy、block placement、寄存器和带宽争用仍由 kernel 与调度决定。小 Decode batch 中 tiles 已小于上限时,调整上限可能完全不改变 grid;大 Prefill 则可能更明显。报告 sweep 时要同时记录实际 rows/tiles/grid。
5. 查表、写 staging 与通信分别落账
对某层、某 rank,假设有 tokens、 个有效本地 heads、head width ,FP8 row 的逻辑 source bytes 与 BF16 staging 写入量为:
实际 staging 还可能包含 padded heads;实际总线 bytes 则受到重复 row、cache 命中、事务粒度和地址翻译影响。source 与 staging 是不同内存路径,不能将两者相加后除以 PCIe bandwidth。随后 TP gather、wkv 和 gate 也要单独计量。
若查表真正受 host 路径带宽限制, 是粗略下界。 必须来自相同 rows 分布和并发拓扑,顺序 H2D copy bandwidth 无法直接校准随机 UVA lookup。跨 socket 的访问还可能受远端 DRAM、socket 互联与 PCIe 中的某一段限制。
| 可检验假设 | 支持它需要看到什么 | 什么结果会削弱它 |
|---|---|---|
| 远端页放置主导等待 | 单次改变页归属,lookup 与跨 socket 流量同步变化,主负载固定 | 页归属确实改变,但 lookup / tail 几乎不变 |
| DP 并发争用 host controller / PCIe | 增加同条件 replicas 后,aggregate lookup bytes 上升且 lookup time 变差 | kernel 已受较低层的计算/launch 限制,host 利用率未同步变化 |
| 预取 lead time 不够 | 最早消费层的 event 尚未完成,且存在真实可提前窗口 | lookup 已赶在消费前完成,等待来自其他依赖 |
| 预取侵占主计算 | wait 减少,但同期前置层变慢,端到端收益消失 | 在相同请求与时钟条件下主计算稳定,端到端同步改善 |
以上都是待 GPU 验证的假设,本轮没有填入测量数值。
6. 最小实验:先放置,再调 overlap
6.1 固定条件与正确性
固定 checkpoint、Runtime SHA、driver/CUDA、TP/SP/DP/EP、实际 EDP group、精度、CUDA Graph 模式、GPU clocks 和请求回放。先确认可容纳表和模型,记录 scheduler 的实际 token batches;“同输入”但不同 batching 不构成同条件 kernel 对照。
本实验的 TTFT 从客户端发出请求计到首 token 到达;ITL 为客户端相邻输出 token 的时间间隔,包含实际 serving 路径。goodput 统计满足固定 TTFT / ITL SLO 的成功请求数除以测量时长。GPU step time 单独记录,不替代这些客户端指标。
用同一组 hash IDs 对照每层查表结果、padding / DEAD_ID,再做请求输出对照,覆盖 chunk boundary、prefix hit 和投机回滚。只有正确性通过,才进入性能对照;组或 backing 改动还要复用官方共享存储、权重加载和 graph replay 测试合同。
6.2 两阶段矩阵
| 阶段 | 一次改变什么 | 保持什么 | 核心观测 |
|---|---|---|---|
| A1 放置 | 集中 node 0 ↔ node 1 | 一份共享表、同 kernel / grid / 回放 | 每表 N0/N1、lookup、跨 socket 与 PCIe 流量、ITL tail |
| A2 放置 | 集中 ↔ 交错 | 相同共享 group、同容量与预取 | controller 负载、lookup 分布、TTFT/ITL |
| A3 容量权衡 | 一份共享 ↔ 每 socket 一份 | 相同 heads、请求与输出精度 | 物理 bytes、查询 collectives、locality、SLO goodput |
| B1 预取 | 提前提交 ↔ 消费时提交 | 同一 UVA lookup、同 background grid 与 staging | 排队、kernel、暴露等待、主计算、端到端 |
| B2 资源 | background grid cap 候选比例 | 固定 A 中的放置、lookup 顺序与 kernel 其他参数 | 实际 grid、前置层回归、tail / goodput |
A1/A2 需在首次分配/注册前控制页策略,并重启分配验证,避免复用已经驻留的 backing。A3 和 B1/B2 是研究补丁对照:stock 没有独立关闭 offloaded 预取的开关。将 CPU offload 关闭会改变权重位置,无法独立回答预取效果;B1 也不能顺便把 background grid 改成 foreground grid。
可审阅的 A1/A2 启动补丁应把“放置”插在现有注册前:所有 peers 完成映射 → leader 为共享对象设置目标 policy 并逐页写入预触页 → CPU group barrier → peers 注册 → 采集页分布 → 按原合同加载权重。逐页初始化的内容随后由 checkpoint 覆盖。这个屏障用于避免 peers 在受控预触页之前开始注册;仍需验证内核/驱动是否保持该放置,以及新增启动开销。它是建议的研究实现,尚未在本轮 GPU 上运行。
先用单 GPU lookup microbenchmark 检验局部机制,再增加两 socket 的同机 DP 并发,最后跑真实 Prefill、Decode 和混合回放。每个性能结果至少区分以下状态:
- 启动:文件创建、注册、权重加载;不混入稳态 TTFT。
- 低复用查询:大范围 rows、固定 seed 与分布;不称为“尚未分配的冷页”,表在启动时已注册并加载。
- 重复查询:固定 row 序列;收益可能来自 cache,与调度 overlap 分开归因。
- 线上负载:固定到达率、请求长度和 SLO,比较成功请求 goodput、拒绝率与 ITL P95/P99;microbenchmark 的 cache 模式不代替线上结果。
6.3 时间线与验收
研究补丁为每个 layer / rank 标记 hash ready、lookup submission / start / done、consumer wait 前后、TP gather 与投影。Nsight 时间线区分 CPU enqueue 和 GPU 执行;CUDA event 只测对应 GPU stream 上的 interval,跨 stream 用同一时间基准并保持原依赖,不在每个阶段加全局 synchronize。
先在无 profiler 条件下做重复的 A/B 或随机顺序对照,报告运行次数、样本数、预热、分位数和噪声范围;Trace 只用于解释变化,若计数器采集造成 kernel replay,应单独标注。PCIe / socket 计数器的方向、窗口和可用性必须记录,不把不可用指标填成 0。
验收顺序是:页分布确实改变 → 查表或等待机制按预期改变 → 主计算与通信没有抵消收益 → 固定 SLO 下的端到端改善可重复。只看到 event wait 变短,或 isolated lookup bandwidth 变高,都还不足以宣布 serving 提速。
7. 当前结论与下一步
已确认的机制是:共享表注册早于加载;NUMA wrapper 可以退化为 CPU-only;GPU 直接进行 UVA lookup;所有本地 Engram layers 的 lookup 进入同一串行 stream;background grid 只提供 program 上限。尚未确认的是具体服务器的页归属、跨 socket 惩罚以及最优 grid cap。
最值得先执行的是 A1 加 B1:先用实际页分布证明放置对照成立,再在同一放置和同一 lookup kernel 下比较提前提交与消费时提交。只有这两步证据齐全,才有依据决定是否付出约 的容量,做每 socket 共享副本。
- Engram TP 分片与同机 DP 共享 — 表、投影、有效 EDP 与容量账。
- 推理并行 — TP/DP/EP 的通信作用域。
- GPU Trace 分析 — 事件、依赖与性能证据。
- 关键路径 — 暴露等待与端到端归因。
被以下页面引用(1)
修改历史1 次提交
- docs: publish Engram NUMA placement and prefetch researchweigao chen··
45492b4