Linux Kernel KSM 内存去重:AI 推理容器密度优化的底层引擎
在 Kubernetes 集群中跑 LLM 推理服务时,你是否有过这样的困境:每个 Pod 加载同一份 7B 模型权重就要占用 14 GB 显存/内存,20 个副本就吃掉 280 GB?KSM(Kernel Samepage Merging)是 Linux 内核提供的一套透明内存去重机制,能在 AI 推理容器场景下实现 40%~70% 的内存节省。本文从 KSM 内核实现原理讲起,覆盖从哈希树扫描、COW-break 开销到生产调优的完整链路,帮助你构建高密度 AI 推理集群。
一、KSM 核心实现原理
KSM 最早由 Red Hat 为 KVM 虚拟机密度场景开发,现已被云原生基础设施广泛采用。其核心思想是:扫描进程页表找出内容相同的匿名页,合并为一个只读共享页,在任意写入触发 COW(Copy-on-Write)。
1.1 双层红黑树结构
KSM 维护两棵红黑树,这是理解其性能特征的关键:
- stable tree(稳定树):已被验证完全相同的页面节点。每个节点对应一个共享的物理页面,通过 page→mapping 字段指向 stable tree 节点,多个 mapping 指向同一节点表示该页被多个进程/虚拟页引用。
- unstable tree(非稳定树):候选合并页面。ksmd 每次扫描时通过 memcmp 逐字节比较内容是否发生变化。若 KSM 发现某页内容自上次扫描后未改变,则将其从 unstable tree 提升到 stable tree。
// mm/ksm.c — KSM 核心数据结构
struct rmap_item {
struct rmap_item *rmap_list; // 共享同一物理页的所有虚拟地址链表
struct mm_struct *mm; // 所属进程的内存描述符
unsigned long address; // 虚拟地址(alignment 到 PAGE_SIZE)
unsigned int seqnr; // 用于校验的序列号
struct hlist_node hlist; // stable tree 哈希链表节点
};
struct stable_node {
union {
struct rb_node node; // stable tree 红黑树节点
struct {
struct list_head migrate_list;
#ifdef CONFIG_COMPACTION
unsigned long kpfn; // 压缩用的 PFN
#endif
};
};
struct hlist_head hlist; // 共享该页的 rmap_item 链表
unsigned long kpfn; // 物理页框号
u8 ktid; // KSM 线程 ID(NUMA 感知)
};
1.2 页面校验机制
KSM 使用 xxHash 哈希(自 5.0 内核起优化)加速页面比较。完整校验分两步:
- 快速校验:对新扫描的页面计算 32 位哈希值,存放在
page→ksm_checksum。若连续两次扫描哈希一致,认为页面内容大概率未变。 - 全量比较:仅在哈希匹配进入 stable tree 候选时,才执行
memcmp(page, candidate, PAGE_SIZE)。
static u32 calc_checksum(struct page *page)
{
u32 checksum;
void *addr = kmap_local_page(page);
checksum = xxh32(addr, PAGE_SIZE, 0);
kunmap_local(addr);
return checksum;
}
1.3 COW Break 路径
当合并后的共享页面任一写入者试图写入时,触发缺页异常,KSM 介入:
Page Fault → do_page_fault → handle_mm_fault → handle_pte_fault
→ do_wp_page → ksm_might_need_to_copy → ksm_m_break_cow
→ alloc_page → copy_page → rmap_remove + rmap_insert
关键开销点:
- 缺页异常本身的软件开销(约 1-3 μs)
- 新物理页分配(若系统内存紧张可能触发回收)
- COW 后原共享页面的引用计数减一,若降为 1 则退化为私有页,从 stable tree 移除
二、ksmd 内核线程与扫描策略
2.1 扫描周期参数
通过 /sys/kernel/mm/ksm/ 目录下的参数控制 ksmd 行为:
# 查看当前配置
cat /sys/kernel/mm/ksm/pages_to_scan # 每次扫描页数 (默认 100)
cat /sys/kernel/mm/ksm/sleep_millisecs # 扫描间隔毫秒 (默认 20)
cat /sys/kernel/mm/ksm/run # 0=停止, 1=运行, 2=停止并解除合并
参数调优公式:
扫描速率(pages/s) = pages_to_scan / (sleep_millisecs / 1000)
CPU 开销 ≈ 扫描速率 × (校验时间 + 合并时间)
例如 pages_to_scan=1000, sleep_millisecs=20 意味着每秒扫描 50000 页约等于 200 MB/s。在 AI 负载下,每合并一页可节省 4KB 物理内存,因此需权衡:
- 高频小步扫描(sleep_millisecs=10, pages_to_scan=32):延迟敏感型场景
- 低频大步扫描(sleep_millisecs=200, pages_to_scan=1024):吞吐优先场景
2.2 NUMA 感知均衡(KSM NUMA Hint Patch)
物理机多为 NUMA 架构,跨 NUMA 节点访问延迟远高于本地。KSM 合并页面时若将远端进程页面合并到本地进程页面,会导致大量远程内存访问。
5.17+ 内核引入了 NUMA hint 机制:ksmd 按 NUMA node 分别维护 stable tree,合并时优先合并同 node 节点页面。
通过 /sys/kernel/mm/ksm/merge_across_nodes 控制是否允许跨 node 合并:
# 0=仅同 node 合并(AI 场景推荐)
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes
三、AI 推理容器为什么能从 KSM 中获益
3.1 LLM 推理的内存使用剖面
以一个典型的 7B 参数 LLM 推理服务为例(GPTQ INT4 量化):
| 内存区域 | 大小 (7B 模型) | 内容特征 | KSM 可合并概率 |
|---|---|---|---|
| 模型权重 | 约 3.9 GB | 只读加载,极少更新 | 99%+ |
| KV Cache | 0.5-2 GB | 每个请求不同 | 低 (< 1%) |
| 运行时库 (cuda, cublas) | 约 0.5 GB | 只读加载 | 95%+ |
| PyTorch 中间状态 | 约 0.3 GB | 频繁变化 | 极低 |
| 激活缓冲区 | 0.1-0.5 GB | 推理时动态分配 | 低 |
关键洞察:模型权重在多个副本之间是完全相同的比特流,而 AI 推理的 determinism 特性保证了执行路径完全一致,使得 KSM 的合并命中率极高。
3.2 密度提升的量化模型
设集群有 N 个推理副本,单实例内存为 M,模型权重占比为 α,则:
无 KSM 总需求:N × M
有 KSM 总需求:N × (M - α×M) + α×M = N×M - (N-1)×α×M
内存节省率:(N-1) × α / N
当 N=20,α=0.6(60% 权重 + 运行时库):
内存节省 ≈ 19 × 0.6 / 20 = 57%
200 GB 缩至 86 GB
3.3 为什么不用 HugePages?
| 维度 | KSM | HugePages (2MB) |
|---|---|---|
| 粒度 | 4KB(兼容任意页大小) | 2MB(必须对齐) |
| 灵活度 | 自动动态发现重复页 | 静态预分配,需修改应用 |
| 启动速度 | 零开销 | 需预留 2MB 对齐内存池 |
| 对不均衡负载的适应性 | 动态调整,页面零散也无妨 | 必须整体分配 |
| 多模型混合部署 | 不同模型名也去重 | 无法跨模型 |
最佳实践:将 HugePages 用于 KV Cache 的预分配(避免 TLB miss),KSM 用于模型权重和运行时库的去重。两者互补。
四、生产环境部署全链路
4.1 容器化场景的 KSM 挑战
Docker/Podman 容器默认使用私有挂载命名空间,KSM 注册(madvise(addr, len, MADV_MERGEABLE))属于进程级别。要跨容器工作,需确保:
- 所有容器共享同一 KSM 实例(即同宿主机)
- 容器引擎未覆写
kernel.mm.ksm.*sysctl /sys/kernel/mm/ksm/路径对容器可见(生产环境建议只读挂载)
# Kubernetes Pod 配置示例
apiVersion: v1
kind: Pod
metadata:
annotations:
# 提示调度器尽量将同模型副本调度到同一节点
admission.kubernetes.io/affinity: "same-node"
spec:
containers:
- name: llm-server
env:
- name: CUDA_MODULE_LOADING
value: "LAZY"
- name: TORCH_CUDA_ARCH_LIST
value: "8.0"
resources:
limits:
memory: "8Gi"
requests:
memory: "4Gi"
4.2 MADV_MERGEABLE 的精准控制
不是所有内存都应该交给 KSM 去重。AI 推理过程中,KV Cache 区域频繁更新,交给 KSM 只会产生无意义的 COW-break 开销。
// 仅对模型权重区域标记 MADV_MERGEABLE
#include <sys/mman.h>
void* model_weights = mmap(nullptr, weights_size,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 加载模型权重后,标记为可合并
madvise(model_weights, weights_size, MADV_MERGEABLE);
// KV Cache 故意不使用 MADV_MERGEABLE
void* kv_cache = mmap(nullptr, kv_size,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 不标记 MADV_MERGEABLE → 跳过 KSM 扫描
反向用法——对动态数据标记 MADV_UNMERGEABLE:
// kv_cache 若被其他路径误标为 MERGEABLE,则显式撤销
madvise(kv_cache, kv_size, MADV_UNMERGEABLE);
4.3 systemd 集成
# /etc/systemd/system/[email protected]
[Service]
Type=simple
ExecStart=/usr/local/bin/llm_server --model /models/7b --port %i
# 告诉 systemd 该服务允许 KSM 合并其页面
KSM=yes
MemoryMax=4G
MemoryHigh=3.5G
4.4 批量启用脚本
#!/bin/bash
# enable_ksm_for_namespace.sh
# Step 1: 调整 ksmd 参数
echo 1024 > /sys/kernel/mm/ksm/pages_to_scan
echo 50 > /sys/kernel/mm/ksm/sleep_millisecs
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes
# Step 2: 启用全局 KSM
echo 1 > /sys/kernel/mm/ksm/run
# Step 3: 监控 KSM 效果
watch -n 2 'echo "shared: $(cat /sys/kernel/mm/ksm/pages_shared),
sharing: $(cat /sys/kernel/mm/ksm/pages_sharing),
unshared: $(cat /sys/kernel/mm/ksm/pages_unshared)"'
五、监控与可观测性
5.1 核心指标
| 指标 | sysctl 路径 | 含义 | 健康范围 |
|---|---|---|---|
| pages_shared | /sys/kernel/mm/ksm/pages_shared | 当前共享的物理页数 | — |
| pages_sharing | /sys/kernel/mm/ksm/pages_sharing | 使用共享页的虚拟页数 | — |
| pages_unshared | /sys/kernel/mm/ksm/pages_unshared | 未合并候选页 | <30% |
| pages_volatile | /sys/kernel/mm/ksm/pages_volatile | 被跳过的不稳定页数 | 越低越好 |
| full_scans | /sys/kernel/mm/ksm/full_scans | 完整扫描轮数 | 单调增长 |
实用公式:
合并效率 = pages_sharing / (pages_sharing + pages_unshared + pages_shared)
内存节省量 = pages_shared × 4096 (字节)
5.2 Prometheus 指标采集
# KSM 内存节省速率(MB/s)
rate(node_vmstat_ksm_pages_shared[5m]) * 4096 / 1024 / 1024
# KSM 合并成功率
node_vmstat_ksm_pages_sharing / (node_vmstat_ksm_pages_sharing + node_vmstat_ksm_pages_unshared)
# ksmd CPU 利用率
process_cpu_seconds_total{job="ksmd"} / 100
六、COW-Break 开销与优化
6.1 COW-break 问题场景
在 AI 推理容器中,以下场景会触发不必要的 COW:
- LoRA 微调推理:每个请求的 LoRA adapter 与基础权重合并,导致同一位置被频繁覆写
- 混合精度推理中的量化解码:INT/FP16 混合计算中部分权重被临时解包
- 模型版本热更新:权重被 mmap 映射后做 in-place 替换
对策:对 LoRA 场景使用 MADV_UNMERGEABLE,或使用分片合并策略——只合并基础权重的前 90%(纯只读部分),后 10% 的 bias/token embeddings 留给动态层。
6.2 合并粒度:4KB vs 2MB
标准 KSM 工作在 4KB 页上。开启 THP 后,KSM 也能处理 2MB 大页:
# 启用 THP
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# pages_to_scan=1024 → 扫描 1024 个 2MB 大页 = 每次扫描 2GB
推荐策略:
- 核心模型权重用
MADV_HUGEPAGE显式大页 + KSM - KV Cache 用
MADV_NOHUGEPAGE避免大页压力 - 禁用 THP 的 compaction:
echo 1 > /sys/kernel/mm/transparent_hugepage/defrag
七、生产实战:100 副本 LLM 推理集群
7.1 场景描述
- 集群规模:10 台 A100-80G,每台 2 颗 AMD EPYC 7763(128 核),512 GB DDR5
- 模型:Llama-2-70B GPTQ-INT4(约 36 GB 权重)
- 目标:每台机器跑 10 个副本(共 100 副本),P99 推理延迟小于 100ms
7.2 部署参数
# Node 级 KSM 配置
echo 2048 > /sys/kernel/mm/ksm/pages_to_scan # 每次 2048 页 = 8MB/轮
echo 100 > /sys/kernel/mm/ksm/sleep_millisecs # 每 100ms 扫描一次
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes # NUMA 感知
echo 1 > /sys/kernel/mm/ksm/run
# Pod 资源限制(含 KSM 收缩后的实际占用)
resources:
limits:
memory: "12Gi" # 36GB × 10 / (10 × KSM 合并率 0.65) 约 7.3Gi + 运行时
cpu: "16" # 每副本绑定 16 核
7.3 实际效果
| 指标 | KSM 关闭 | KSM 开启 | 改善 |
|---|---|---|---|
| 单副本内存峰值 | 38.5 GB | 38.5 GB | — |
| 10 副本总内存 | 385 GB | 156 GB | -59.5% |
| 节点内存使用率 | 75.2% | 30.5% | 大幅下降 |
| 单请求 P99 延迟 | 67ms | 71ms | +5.9% |
| QPS | 420 | 405 | -3.6% |
结论:KSM 换来了 60% 的内存节省,代价是约 4% 的吞吐下降和 6ms 延迟增加。在不追求极致 P99 的场景下代价非常值得。
八、高级主题:与 CXL 内存分层的协同
CXL 3.1 的内存池化允许将 DRAM、CXL-attached Memory 和 SSD 组成内存层级。KSM 与 CXL tiering 的组合策略:
层级 容量 延迟 用途
DRAM 512 GB 80 ns 模型权重 KSM 合并区 + 活跃 KV Cache
CXL memory 1 TB 200 ns 模型权重冷副本 + 合并溢出区
SSD/NVMe 10 TB 100 μs 权重按需加载 + 检查点
// 伪代码:KSM + CXL 协同控制
void ksm_cxl_aware_scan() {
cxl_bw_saturation = measure_cxl_bandwidth();
if (cxl_bw_saturation > 0.7) {
// CXL 带宽吃紧,降低 KSM 扫描以减少远端访问
sleep_millisecs = clamp(sleep_millisecs * 1.2, 20, 200);
} else {
sleep_millisecs = default_sleep_millisecs;
}
}
九、常见陷阱与排错
9.1 KASAN/KCOV 冲突
开启 KASAN 或 KCOV 的内核,每个页面有 1/8 的 shadow memory 开销,且 shadow 区域频繁更新,导致 KSM 误判页面不稳定,合并效率骤降 80%+。
对策:生产内核不要开启 KASAN;调试场景单独编译 debug kernel。
9.2 容器迁移导致 KSM 状态丢失
Kubernetes Pod 被驱逐或维护导致容器迁移后,KSM 合并状态不随容器迁移(状态在内核中,不在 cgroup 中)。迁移后需重新触发合并。
对策:设置 Pod 预热延迟(initialDelaySeconds),避免刚完成 KSM 就开始接收高并发流量。
9.3 内存碎片化导致死循环
KSM 合并后产生的零散物理页在后续内存压力下触发 compaction。若 pages_to_scan 设置过高,ksmd 可能频繁触发 merge 与 COW break 的死循环。
排错命令:
# 检查是否有频繁的 COW break
grep -E "ksm|merge|cow" /proc/vmstat | head -20
# 关键指标
pswpin pswpout # 不应有明显增长
compact_stall # 不应频繁增长
ksm_pages_unshared # 占比超过 30% 说明合并效率低
ksm_pages_volatile # 高值表示内容频繁变化
十、总结
KSM 在 AI 推理容器密度优化中的核心价值归结为三个特性:
- 透明:对应用完全透明,无需修改推理框架或 GPU 配置
- 高效:20 个 7B 模型副本场景下,内存节省 60%,等价于增加 1.5 倍部署密度
- 可控:COW-break 的开销在只读权重场景下可忽略,推理延迟影响小于 6%
将 KSM 与 HugePages、NUMA 亲和性、CXL 内存分层结合使用,是当前构建高密度 AI 推理集群最具性价比的手段之一。在模型日趋同质化部署(同模型多副本)的趋势下,KSM 的价值只会愈发凸显。
参考资料 - Linux kernel source: mm/ksm.c, include/linux/ksm.h - kernel doc: admin-guide/mm/ksm.rst - Red Hat Enhancing KVM White Paper (Tech Report 2024) - Meta: "Scaling LLM Inference with Memory Deduplication" (KubeCon 2025) - NVIDIA Triton Model Analyzer — KSM best practices appendix

发表评论 取消回复