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 内核起优化)加速页面比较。完整校验分两步:

  1. 快速校验:对新扫描的页面计算 32 位哈希值,存放在 page→ksm_checksum。若连续两次扫描哈希一致,认为页面内容大概率未变。
  2. 全量比较:仅在哈希匹配进入 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))属于进程级别。要跨容器工作,需确保:

  1. 所有容器共享同一 KSM 实例(即同宿主机)
  2. 容器引擎未覆写 kernel.mm.ksm.* sysctl
  3. /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:

  1. LoRA 微调推理:每个请求的 LoRA adapter 与基础权重合并,导致同一位置被频繁覆写
  2. 混合精度推理中的量化解码:INT/FP16 混合计算中部分权重被临时解包
  3. 模型版本热更新:权重被 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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }