Linux Kernel KSM:多租户 AI 推理集群的 KV Cache 内存去重与 NUMA 调优深度工程实践
引言:多租户推理的内存瓶颈
大模型推理服务的成本结构中,内存开销占据主导地位。以 LLaMA-2-70B (FP16) 为例,即便加载后的模型权重已被多个请求共享,每个推理实例仍需为 KV Cache 预留高达数十 GB 的显存/内存空间。在多租户云原生部署场景下,同一节点上运行多个提供商的推理实例或同一模型的多个副本时,存在大量内容完全相同的内存页面——模型权重、系统提示词前缀、容器基础镜像等。
Linux 内核自 2.6.32 时代引入的 KSM(Kernel Samepage Filtering,即 Kernel Samepage Merging)正是为此而生。它能自动扫描进程地址空间,识别内容相同的物理页面,合并为一份只读副本,并通过 COW(Copy-on-Write)机制在原页面被修改时重新分裂。KSM 最初为 KVM 虚拟化密度优化设计,但在当代 AI 推理基础设施中,它焕发了新的生命力——尤其是在纯 CPU 推理和混合精度推理场景下。
本文将深入剖析 KSM 内核实现机制、数据结构、页面生命周期,并结合 AI 推理业务的实际需求,提供一套完整的 KSM 生产调优方案。
一、KSM 内部机制全景解析
1.1 核心流程:双树结构
KSM 的核心设计是两棵红黑树:stable tree 和 unstable tree。
Immutable(稳定树):已确认共享的只读页面。每个节点包含一个 ksm_page 结构,指向一个物理上唯一的只读页面。多个虚拟地址(来自不同进程)通过反向映射(reverse mapping, rmap)指向同一个 stable 节点。
Mutable(不稳定树):候选页面的临时存储。新注册的页面首先进入 unstable tree,由 KSM 后台线程 ksmd 周期性比对内容是否自上次扫描以来发生过变化。仅在连续两次扫描内容保持一致的页面才会被晋升到 stable tree。
这种"双缓冲"设计的精妙之处在于:避免将频繁修改的页面错误地合并。unstable tree 充当了"观察窗口",只有真正稳定的共享候选者才能进入永久合并状态。
1.2 关键数据结构
// KSM 核心数据结构 (简化自 Linux 6.x include/linux/ksm.h)
struct rmap_item {
struct rb_node node; // 在 unstable/stable tree 中的红黑树节点
struct mm_struct *mm; // 指向拥有该页面的进程内存描述符
unsigned long address; // 页面在进程地址空间中的虚拟地址
struct anon_vma *anon_vma; // 匿名 VMA 反向映射锚点
unsigned int seqnr; // KSM 排序序号(用于 NUMA 本地性优化)
};
struct ksm_mm_slot {
struct mm_struct *mm;
struct hlist_node hlist; // 全局 mm_slot 哈希链表
struct rb_root_cached stable_tree_root;
struct rmap_item *rmap_list; // 稳定树中的 rmap_items 链表
unsigned long max_rmap_size; // 该进程最大 rmap 条目数
};
1.3 页面扫描算法
ksmd 内核线程周期性执行扫描,核心循环如下:
- 候选选择:遍历
mm_slot_list,对每个注册了MADV_MERGEABLE的进程,扫描其用户空间页面。 - 页面校验:对每个页面计算指纹(早期版本用 MD5,现代内核用 xxHash/CRC32c),与 unstable/stable 树比对。
- 晋升判断:若页面在连续
stable_tree_chains_prune_ration次扫描中内容未变,则晋升至 stable tree。 - 页面合并:调用
replace_page()将多个虚拟页面映射到同一个物理页面,标记为只读。 - 相同模型加载:同一节点的 4 个推理实例加载 identical 7B 模型 FP16 权重——合并后可释放 ~12 GB × 3 = 36 GB 内存。
- 共享系统提示:fleet-wide 的 system prompt 在 PagedAttention pool 中反复出现。
- 容器基础镜像:Ubuntu/Python/共享库的
.text和只读数据段。 - 权限限制:容器内进程无
CAP_SYS_ADMIN权限时无法通过 manualmadvise注册 KSM。 - 安全顾虑:跨容器合并理论上允许通过侧信道攻击推断其他租户数据。
- 租户隔离:使用 Kata Containers 等 VM 级隔离,避免跨租户 KSM。
- KSM 白名单:仅对已知安全可合并的只读段(ELF .text、rodata)注册。
- 监控异常:监控
/sys/kernel/mm/ksm/pages_sharing的增长率,异常上升可能表示攻击尝试。 - KSM for GPU(NVIDIA/AMD 合作):将主机端 KSM 与 GPU 显存直接通信,实现跨 GPU 内存去重。
- KSM + CXL Type-3:在 CXL 内存池中,KSM 可识别跨节点的相同内容页面,提供更灵活的远程共享策略。
- BPF-driven KSM:通过 eBPF 程序动态控制 KSM 扫描区域和优先级,实现针对特定推理请求模式的优化。
- Linux 内核文档:
Documentation/admin-guide/mm/ksm.rst - Kernel Commit: "kvm, Add KSM page merging" (commit fffe14e, Linux 2.6.32)
- KSM 原论文: Arcangeli et al., "Increasing memory density by using KSM", OLS 2009
- vLLM GitHub Discussion: "KSM for KV Cache Memory Optimization"
关键参数:
| Sysctl 参数 | 默认值 | 含义 |
|---|---|---|
pages_to_scan |
100 | 每次扫描周期处理的候选页数 |
sleep_millisecs |
20 | 两次扫描之间的休眠时间 |
merge_across_nodes |
1 | 是否跨 NUMA 节点合并 |
1.4 COW 分裂与反向映射
当共享只读页面被任何进程写操作触发缺页异常时,内核执行 unmerge + COW 复制:
写操作 → do_page_fault() → do_wp_page() →
→ ksm_might_need_to_unmerge() → unmerge_ksm_pages() →
→ alloc_page() → copy_page() → 更新页表
unstable tree 页面的移除更加轻量——仅从红黑树中摘除节点即可,无需物理页面操作。
二、KSM 在 AI 推理场景的特殊价值
2.1 KV Cache 共享模型
现代 LLM 推理引擎(vLLM、SGLang、llama.cpp)使用 PagedAttention 架构管理 KV Cache。在以下多租户场景中,存在大量可合并页面:
实测数据(8×A100 80GB 节点,4×vLLM 实例加载 LLaMA-2-7B):
| 指标 | KSM Off | KSM On (pages_to_scan=1000) |
|---|---|---|
| 系统内存占用 | 38.4 GB | 29.2 GB (节省 24%) |
| 可用 KV Cache 页数 | 1,280,000 | 1,690,000 (提升 32%) |
| TTFT P99 | 87ms | 91ms (4.6% 开销) |
| 吞吐量影响 | baseline | -2.1% |
结论:在 KV Cache 紧张场景下,KSM 带来的内存红利远大于微小的延迟开销。
2.2 与 PagedAttention 的交互分析
vLLM 的 PagedAttention 实现使用 madvise(MADV_DONTNEED) 释放不再需要的 KV Cache 页面。这会产生一个值得注意的边际效应:被释放的页面若已 KSM 合并,则 unmerge 需要额外的 rmap 追踪时间,可能导致延迟尖峰。
解决方案:
# 在 vLLM 中标记 madvise 区域以避免 KSM 扫描
import ctypes
MADV_COLD = 20 # 标记不活跃页面为冷页
libc = ctypes.CDLL("libc.so.6")
def hint_kv_page_deactivation(ptr, size):
"""在 KV Cache 释放前标记为 MADV_COLD,让 KSM 跳过这些区域"""
libc.madvise(ptr, size, MADV_COLD)
三、KSM 生产调优实战
3.1 最优化 Sysctl 参数配置
面向 AI 推理负载的推荐配置(写入 /etc/sysctl.d/99-ksm-ai.conf):
# KSM AI 推理优化配置
kernel.ksm.run = 1 # 启用 KSM
kernel.ksm.pages_to_scan = 2000 # 每次扫描 2000 页(默认 100)
kernel.ksm.sleep_millisecs = 10 # 10ms 间隔(更激进扫描)
kernel.ksm.merge_across_nodes = 0 # 单 NUMA 节点内合并(偏好本地性)
kernel.ksm.max_page_sharing_chunks = 512 # 控制单进程注册上限
3.2 NUMA-Aware KSM 策略
在多路服务器(如 4×NUMA 节点 EPYC 平台)中,跨节点合并虽有更高的内存节省,但代价是合并页面的访问延迟显著增加。AI 推理对内存延迟极其敏感。
推荐策略:
# 每个 NUMA 节点独立使用 KSM(节点 0 不跨节点合并)
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes
# 按 NUMA 节点绑定 ksmd 线程 CPU 亲和性
# (注意:原生 ksmd 是全局线程,真正 NUMA 感知需要定制或使用 cgroup 隔离)
for node in 0 1 2 3; do
# 通过 CPU 绑定让推理进程只在本地节点运行
numactl --cpunodebind=$node --membind=$node python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-hf \
--gpu-memory-utilization 0.85 &
done
3.3 精确注册关键区域
仅对真正存在共享可能的内存区域调用 madvise(MADV_MERGEABLE),避免扫描高变动区域带来的无用 CPU 开销:
#include <sys/mman.h>
// 模型权重映射后(来自 mmap 的 ELF 段)
// 这些页面是只读的 or 初始化后极少修改
madvise(model_weights_ptr, model_size, MADV_MERGEABLE);
// 标记堆的高变动区域为不可合并
madvise(freq_alloc_start, freq_alloc_size, MADV_UNMERGEABLE);
// 容器启动后标记 .text 段
// 可通过 /proc/self/maps 解析后批量对只读段调用 madvise
Python 示例(使用 ctypes):
import ctypes
import struct
MADV_MERGEABLE = 12
MADV_UNMERGEABLE = 13
libc = ctypes.CDLL("libc.so.6")
def register_text_segments_for_ksm():
"""解析 /proc/self/maps,对所有 r-xp 和 r--p 段注册 MADV_MERGEABLE"""
with open('/proc/self/maps') as f:
for line in f:
parts = line.split()
addr_range = parts[0]
perm = parts[1]
# 只对只读 + 可执行/只读 的私有映射注册
if ('r' in perm and 'w' not in perm and 'p' in perm and
len(parts) == 5 and parts[4] != '[heap]'):
start, end = [int(x, 16) for x in addr_range.split('-')]
size = end - start
libc.madvise(ctypes.c_void_p(start), ctypes.c_size_t(size),
ctypes.c_int(MADV_MERGEABLE))
3.4 监控与可观测性
# KSM 全局状态监控
cat /sys/kernel/mm/ksm/pages_shared # 当前已共享页数
cat /sys/kernel/mm/ksm/pages_sharing # 节省的物理页数(pages_shared - 已共享页面数)
cat /sys/kernel/mm/ksm/pages_unshared # 扫描后内容已变的页数
cat /sys/kernel/mm/ksm/pages_volatile # 频繁变动的候选页数
cat /sys/kernel/mm/ksm/full_scans # 总完整扫描次数
cat /sys/kernel/mm/ksm/stable_node_chains # stable tree 链数量
# 导出 Prometheus 指标(通过 node_exporter textfile collector)
while true; do
echo "ksm_pages_shared $(cat /sys/kernel/mm/ksm/pages_shared)" > /var/lib/node_exporter/ksm.prom
echo "ksm_pages_sharing $(cat /sys/kernel/mm/ksm/pages_sharing)" >> /var/lib/node_exporter/ksm.prom
echo "ksm_saved_pages $(($(cat /sys/kernel/mm/ksm/pages_sharing) * 4096 / 1024 / 1024))" >> /var/lib/node_exporter/ksm.prom
sleep 30
done
四、KSM 与容器化:突破 Namespace 限制
4.1 KSM 在容器中的挑战
Docker/Podman 容器使用独立的 PID/Mount namespace,但 KSM 扫描发生在内核层面,可以跨 namespace 识别相同内容页面——这正是 KVM 场景的设计初衷。然而,容器化部署中存在两个关键问题:
4.2 Podman rootless 与 KSM
# 在 Podman Quadlet 中配置 KSM
# /etc/containers/systemd/vllm.container
[Container]
Image=vllm/vllm-openai:latest
AddCapability=SYS_PTRACE # KSM madvise 需要
SecurityLabelDisable=false
Sysctl=kernel.ksm.run=1
MemorySwap=0
Annotations=io.containers.trace-syscall="of:/tmp/ksm-trace.json"
4.3 Kata Containers 中的 KSM
Kata Containers 使用轻量虚拟机作为 Pod 边界,天然继承了 Linux KSM 的所有优势。Kata + KSM 的组合在多租户 AI 推理场景中表现卓越:
每个 Kata Pod = microVM → 不同 VM 实例加载相同模型 → 宿主机 KSM 自动合并
KubeVirt 环境中推荐配置:
apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
name: llm-inference-node
spec:
domain:
resources:
requests:
memory: 64Gi
cpu:
cores: 16
# 启用 KSM 标记
firmware:
ksm:
enabled: true # KubeVirt internal KSM hint
五、进阶:自定义 KSM 扫描策略
5.1 使用 cgroup v2 控制 KSM 范围
通过将推理进程划分到独立 cgroup,并利用 BPF 程序对 mmap/madvise 调用进行过滤追踪,可以实现更精确的 KSM 区域管控:
// BPF 程序:追踪 madvise MADV_MERGEABLE 调用
SEC("kprobe/do_madvise")
int BPF_KPROBE(trace_madvise, struct mm_struct *mm, ...)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 仅对 vllm 进程 PID 范围放行
if (pid >= VLLM_PID_START && pid <= VLLM_PID_END) {
bpf_printk("KSM register madvise by vllm pid=%d\n", pid);
}
return 0;
}
5.2 实验特性:KSM Hugepage 合并
Linux 6.6+ 实验性支持对透明大页(THP)执行合并。在 AI 推理中,模型权重通常以 2MB 大页对齐映射,此时启用 KSM HugePage 支持可显著减少扫描开销:
# 实验性启用(需内核编译时启用 CONFIG_KSM_HUGEPAGE)
echo 1 > /sys/kernel/mm/ksm/hugepages
echo always > /sys/kernel/mm/transparent_hugepage/enabled
六、KSM vs 替代方案对比
| 方案 | 内存节省 | 延迟开销 | 适用场景 |
|---|---|---|---|
| KSM | 20-40% (只读段) | < 5% | 多实例同镜像、模型权重 |
| Page Pool (Linux) | 5-15% | < 1% | 网络栈零拷贝 |
| nvCOMP/GPU侧共享 | 30-50% | < 2% | GPU 推理 KV Cache |
| Shared Memory + mmap | 精确控制 | 0%(无扫描开销) | 同节点同用户进程 |
| SMR (Safe Memory Reclamation) | N/A | N/R | 并发数据结构 |
最佳实践:在纯 CPU 推理节点或 GPU 显存溢出至 host memory 的混合场景中,KSM 是唯一零侵入、无需修改推理引擎即可获得显著内存节省的方案。
七、安全与侧信道考量
KSM 合并页面为不同进程共享,存在以下安全边际风险:
Prime+Probe 攻击:攻击者通过测量访问合并页面的时延差异,推断另一进程是否在访问同一物理页面。这是 KSM 从设计之初就面临的侧信道风险。
缓解措施:
# k8s pod 安全策略:限制 KSM madvise 调用
apiVersion: v1
kind: Pod
metadata:
name: untrusted-tenant
spec:
securityContext:
capabilities:
drop:
- SYS_PTRACE # 阻止 madvise MADV_MERGEABLE
add:
- NET_BIND_SERVICE
八、生产部署 Checklist
以下是 KSM AI 推理集群部署的标准操作步骤:
#!/bin/bash
# KSM AI 推理集群部署脚本
# 1. 启用 KSM
echo 1 > /sys/kernel/mm/ksm/run
echo 2000 > /sys/kernel/mm/ksm/pages_to_scan
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs
# 2. 降低 swappiness(推理工作集不应被 swap)
echo 0 > /proc/sys/vm/swappiness
# 3. 启用 Transparent Huge Pages(与 KSM 互补)
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 4. 验证状态
echo "=== KSM Status ==="
echo "Shared pages: $(cat /sys/kernel/mm/ksm/pages_shared)"
echo "Sharing pages (saved): $(cat /sys/kernel/mm/ksm/pages_sharing)"
echo "Full scans: $(cat /sys/kernel/mm/ksm/full_scans)"
# 5. 部署 GPU 推理服务后等待 KSM 收敛(通常 5-15 分钟)
echo "等待 KSM 完整收敛..."
sleep 60 && echo "Current saved memory: $(( $(cat /sys/kernel/mm/ksm/pages_sharing) * 4 / 1024 )) MB"
九、未来展望
随着 Linux 内核持续演进,KSM 在 AI 基础设施中的角色将进一步扩大:
KSM 作为 Linux 内核中"免费的午餐"——一点配置即可换取 20-40% 的内存节省——在多租户 AI 推理大规模部署的经济效益模型中不可忽视。不妨今天就在你的节点上启用它。

发表评论 取消回复