内核机制与 AI 系统性能
为什么大模型系统要关心 NUMA?
大模型训练和推理不是“GPU 自己算”这么简单,而是 CPU、内存、GPU、PCIe/NVLink、网卡、磁盘 I/O 的协同调度问题。GPU 算力很强,但如果 Linux 内核侧的 NUMA、CPU 绑核、内存分配和设备亲和性不合理,就会出现 GPU 等数据、CPU 争抢、远端内存访问、H2D 拷贝变慢、RDMA 抖动等问题。
NUMA 是 Non-Uniform Memory Access,非统一内存访问架构。在多 Socket 服务器中,每个 CPU Socket 通常有自己直连的内存控制器和本地内存。
Socket 0 ── 本地内存 0
Socket 1 ── 本地内存 1
CPU 访问自己 Socket 直连的内存,叫 local memory access;访问另一个 Socket 连接的内存,叫 remote memory access。
| 访问类型 | 延迟 | 带宽 | 额外影响 |
|---|---|---|---|
| 本地内存访问 | 低 | 高 | 不占用 Socket 间互联 |
| 远端内存访问 | 高 | 低 | 占用 UPI/QPI/Infinity Fabric 等 Socket 间链路 |
Socket 间通常通过 UPI、QPI、Infinity Fabric 等互联链路通信。这个链路的带宽和延迟通常弱于本地内存控制器,因此远端访问会带来明显性能损耗。
跨 Socket 访问会怎样?
假设一个训练进程的 CPU 线程绑定在 Socket 0:
CPU Thread → Socket 0
但它的大量内存页实际分配在 Socket 1:
Memory Pages → Socket 1
访问路径会变成:
后果包括:
- 内存访问延迟升高:CPU 每次访问远端内存都要跨 Socket。
- 有效内存带宽下降:本地 DRAM 带宽不能充分利用,访问受限于 Socket 间互联。
- Socket 间链路拥塞:多个进程跨 Socket 访问会挤占 UPI/QPI/IF。
- DataLoader 性能下降:解码、预处理、batch 拼接依赖 CPU 和内存带宽,远端访问会导致 GPU 等数据。
- GPU-NIC 通信路径变差:GPU 或 NIC 挂在 Socket 0,但 buffer 在 Socket 1,会让 H2D、RDMA 或 staging 路径跨 Socket。
典型糟糕路径:
这会影响 CPU 到 GPU 的 H2D 拷贝、GPU 到 CPU pinned memory 的 staging、RDMA 网卡收发、数据加载吞吐和多卡训练通信稳定性。
多卡机器上的 NUMA 绑定原则
核心原则是:
> 让 CPU 线程、内存、GPU、NIC 尽量落在同一个 NUMA domain / Socket 附近。
一台 8 卡服务器的拓扑可能类似:
Socket 0
├── CPU cores 0-63
├── Memory node 0
├── GPU 0, GPU 1, GPU 2, GPU 3
└── NIC 0
Socket 1
├── CPU cores 64-127
├── Memory node 1
├── GPU 4, GPU 5, GPU 6, GPU 7
└── NIC 1
比较好的绑定方式是:
rank 0 → GPU 0 → Socket 0 CPU cores → NUMA node 0 memory
rank 1 → GPU 1 → Socket 0 CPU cores → NUMA node 0 memory
rank 4 → GPU 4 → Socket 1 CPU cores → NUMA node 1 memory
rank 5 → GPU 5 → Socket 1 CPU cores → NUMA node 1 memory
要避免:
rank 0 → GPU 0 挂 Socket 0
CPU 线程 → Socket 1
内存页 → NUMA node 1
这种绑定会导致 CPU 数据处理、H2D 拷贝、GPU-NIC 通信都可能跨 Socket。
常用 NUMA 绑定方法
使用 numactl
将进程绑定到 NUMA node 0:
numactl --cpunodebind=0 --membind=0 python train.py
含义是:
CPU 尽量使用 node 0
内存也从 node 0 分配
如果是 8 卡机器,可以按 rank 或 GPU 分组:
CUDA_VISIBLE_DEVICES=0,1,2,3 numactl --cpunodebind=0 --membind=0 python train.py
CUDA_VISIBLE_DEVICES=4,5,6,7 numactl --cpunodebind=1 --membind=1 python train.py
结合 taskset
taskset 可以绑定 CPU core:
taskset -c 0-31 python train.py
但它只管 CPU affinity,不直接管内存分配。实际生产中常常组合使用:
numactl --membind=0 taskset -c 0-31 python train.py
容器中通过 cpuset + device 分配
在 Kubernetes 或容器环境中,通常通过这些能力保证拓扑接近:
- CPU cpuset
- memory NUMA policy
- GPU device assignment
NVIDIA_VISIBLE_DEVICES- Topology Manager
- Device Plugin
目标是让容器的 CPU cores、内存、GPU、NIC 在拓扑上接近。
排查和观测
| 目标 | 命令 | 看什么 |
|---|---|---|
| 查看 NUMA node | numactl --hardware | CPU core、内存 node 分布 |
| 查看 GPU/NIC 拓扑 | nvidia-smi topo -m | GPU-GPU、GPU-NIC、GPU-CPU 亲和性 |
| 查看进程 NUMA 分布 | numastat -p <pid> | 进程内存页落在哪些 node |
| 查看 CPU 亲和 | taskset -pc <pid> | 进程允许在哪些 CPU core 运行 |
| 查看系统拓扑 | lstopo | CPU、PCIe、GPU、NIC 的完整层级 |
排查训练慢时,如果 GPU Util 周期性掉低,要同时看 CPU worker 是否跨 NUMA、page cache 是否抖动、H2D 是否跨 Socket、NIC 是否和 GPU 不亲和。
cgroups 是什么?
cgroups 是 Linux 内核提供的资源控制机制,全称是 control groups。它可以限制、统计、隔离一组进程的资源使用。
常见资源包括:
- CPU
- 内存
- I/O
- 进程数
- 设备访问权限
- 网络优先级
容器本质上大量依赖:
namespace + cgroups + capabilities + seccomp
其中:
- namespace 负责“看见什么”。
- cgroups 负责“能用多少”。
- capabilities/seccomp 负责“能做什么”。
cgroups v1 与 v2
cgroups v1
cgroups v1 是多层级、多 controller 模型:
/sys/fs/cgroup/cpu/...
/sys/fs/cgroup/memory/...
/sys/fs/cgroup/blkio/...
/sys/fs/cgroup/devices/...
不同资源 controller 可以有不同的 cgroup 树。优点是灵活、历史兼容性好;缺点是层级复杂,不同 controller 行为不统一。
cgroups v2
cgroups v2 是统一层级模型:
/sys/fs/cgroup/...
所有 controller 在同一棵树上协同工作。它的接口更统一,语义更清晰,也更适合现代容器运行时和 systemd 管理。
| 维度 | cgroups v1 | cgroups v2 |
|---|---|---|
| 层级模型 | 多 hierarchy | 统一 hierarchy |
| controller 行为 | 不同 controller 差异大 | 语义更统一 |
| 容器生态 | 历史兼容多 | 现代发行版逐步默认 |
| AI Infra 关注 | 老集群常见 | 新集群、systemd、K8s 新版本更常见 |
CPU 限制:quota、weight、cpuset
CPU 资源常见控制方式包括三类。
CPU quota / period
限制一段周期内最多可用多少 CPU 时间。
例如 cgroups v2:
cpu.max = 200000 100000
含义可以理解为:
每 100ms 周期内最多使用 200ms CPU 时间
也就是最多约等于 2 个 CPU core。
CPU weight / shares
用于相对权重分配。多个 cgroup 竞争 CPU 时,权重高的获得更多 CPU 时间。它不是硬上限,而是竞争时的比例。
cpuset
限制进程只能运行在哪些 CPU core 上。
cpuset.cpus = 0-31
表示这个 cgroup 只能使用 0 到 31 号 CPU。在大模型训练中,cpuset 很重要,因为它可以配合 NUMA 绑定,让某个训练进程只使用靠近目标 GPU 的 CPU cores。
内存限制:memory.max、memory.high、swap
内存 controller 可以限制:
- 最大内存使用量
- swap 使用
- 内存压力
- OOM 行为
- page cache 使用
典型限制包括:
memory.max
memory.high
memory.swap.max
其中:
memory.max是硬限制,超过后可能触发 OOM。memory.high是软限制,超过后会触发 reclaim 和 throttling。memory.swap.max控制 swap 使用。
在训练/推理场景中,如果容器内存限制过紧,可能出现:
- DataLoader worker 被 OOM kill。
- page cache 不够导致权重加载变慢。
- 频繁 reclaim 导致吞吐抖动。
- pinned memory 分配失败。
- 推理服务 P99 因内存回收而升高。
I/O 限制:带宽、IOPS、权重
I/O controller 可以限制块设备读写,例如:
- 读带宽上限
- 写带宽上限
- IOPS 上限
- I/O 权重
典型场景:
一个推理服务正在加载 100GB 权重
另一个训练任务正在读取海量样本
如果没有 I/O 隔离,可能互相影响:
- 权重加载变慢。
- 训练数据读取抖动。
- page cache 被冲掉。
- 延迟 P99 升高。
因此生产集群中经常需要对不同任务做 I/O QoS,例如训练任务、在线推理服务、模型分发服务、checkpoint 写入任务不能无约束地抢同一块盘或同一个网络存储。
cgroups 怎么感知 GPU?
严格说:
> Linux cgroups 原生并不知道“GPU 算力百分比”这种资源。
cgroups 对 GPU 的管理主要不是通过“限制 GPU SM 使用率”实现的,而是通过 设备访问控制 和 容器运行时注入 实现的。
路径一:devices controller
Linux 中 GPU 设备通常表现为字符设备文件:
/dev/nvidia0
/dev/nvidia1
/dev/nvidiactl
/dev/nvidia-uvm
/dev/nvidia-uvm-tools
cgroups 的 devices controller 可以控制进程是否允许访问这些设备。
例如,容器只被允许访问:
/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-uvm
那么它就只能看到或使用 GPU 0。这种方式控制的是:
能不能打开某个 GPU 设备文件
不是直接控制:
GPU 算力使用 30%
GPU 显存最多 20GB
GPU HBM 带宽最多 50%
路径二:NVIDIA Container Toolkit
容器中使用 GPU 通常依赖 NVIDIA Container Toolkit。它会根据环境变量或容器配置,把对应 GPU 设备、驱动库、运行时依赖挂载到容器中。
常见控制变量包括:
NVIDIA_VISIBLE_DEVICES
NVIDIA_DRIVER_CAPABILITIES
例如:
docker run --gpus '"device=0,1"' ...
容器内通常只会看到指定 GPU。在 Kubernetes 中,通常通过:
NVIDIA Device Plugin
Kubelet
Container Runtime
NVIDIA Container Toolkit
协同实现 GPU 分配。
GPU 显存和算力怎么隔离?
cgroups 本身通常不直接精细限制 GPU SM、Tensor Core、HBM 带宽或显存容量;这些通常需要 GPU 驱动、MIG、MPS、容器运行时和上层调度器协同完成。
| 方案 | 隔离对象 | 优点 | 局限 |
|---|---|---|---|
| 整卡分配 | 一张或多张完整 GPU | 简单、隔离相对好 | 资源利用率可能低 |
| MIG | GPU 硬件实例 | 硬件级切分,显存/算力相对隔离 | 仅特定 GPU 支持,规格固定 |
| MPS | 多进程共享 GPU | 降低上下文切换,提高小 kernel 并发 | 隔离弱于 MIG |
| time-slicing | 时间片共享 | 简单,适合开发/低优任务 | 性能抖动明显 |
| 框架限制 | 进程级显存策略 | 易用,例如 PyTorch fraction | 不是真正硬隔离 |
| 调度器记录 | 上层资源账本 | 能做配额和准入控制 | 依赖平台实现 |
虚拟内存、物理内存和 MMU
虚拟内存 是操作系统给每个进程提供的独立地址空间。进程看到的是虚拟地址,不是直接的物理 DRAM 地址。
虚拟内存带来的好处包括:
- 隔离性:进程 A 不能随便访问进程 B 的内存。
- 地址空间连续:进程以为自己拥有连续地址,但底层物理页可以是离散的。
- 按需分配:
malloc后不一定马上分配物理内存,可能等第一次访问时触发 page fault。 - 支持 mmap、共享内存、文件映射:文件可以映射到进程地址空间。
物理内存 就是真实 DRAM。操作系统把物理内存切成页,常见页大小是:
4KB
虚拟地址到物理地址的映射关系由页表维护。
MMU 是 Memory Management Unit,内存管理单元。它负责把 CPU 发出的虚拟地址转换成物理地址:
为了加速地址转换,CPU 里有 TLB,即 Translation Lookaside Buffer,可以理解为页表转换缓存。
| 情况 | 代价 |
|---|---|
| TLB hit | 虚拟地址到物理地址转换很快 |
| TLB miss | 需要 page table walk,开销更高 |
Huge Pages 为什么有用?
普通页通常是 4KB。Huge Page 可以是:
2MB
1GB
使用大页的好处是:
- 同样大小的内存,需要更少页表项。
- TLB 覆盖范围更大。
- TLB miss 更少。
- page table walk 开销更低。
例如映射 1GB 内存:
| 页大小 | 需要页数量 |
|---|---|
| 4KB page | 262144 个页 |
| 2MB huge page | 512 个页 |
| 1GB huge page | 1 个页 |
对大模型训练/推理,动辄几十 GB 到几百 GB 的 host memory、KV cache staging、权重加载缓存、数据集缓存,大页可能减少 TLB 压力。
THP:透明大页
Transparent Huge Pages,THP 是 Linux 的透明大页机制。它的目标是:
应用程序不显式申请 huge page
内核自动尝试把普通 4KB 页合并成 2MB 大页
THP 的优点是使用方便,应用无需修改代码。但它的问题是:透明不等于免费。
内核可能在运行时做:
- page fault 时分配大页;
- 后台
khugepaged合并页面; - 内存碎片整理 compaction;
- 页面拆分 split。
这些操作可能引入延迟抖动。
为什么深度学习系统中经常建议关闭 THP?
很多在线服务、数据库、低延迟推理系统会建议关闭 THP,原因不是“大页一定不好”,而是 THP 的自动行为可能不稳定。
THP 可能导致延迟尖刺
当内核尝试分配 2MB 连续物理内存时,如果内存碎片严重,可能触发 compaction。
这会导致:
- 请求延迟突然升高;
- DataLoader 卡顿;
- 推理 P99/P999 抖动。
THP 的收益不稳定
深度学习系统里有大量内存分配模式:
- 小对象;
- 临时 buffer;
- pinned memory;
- DataLoader batch;
- mmap 权重;
- CUDA runtime 内存;
- 通信 buffer。
不一定都适合自动大页。
可能影响内存回收
大页拆分、合并、回收比普通 4KB 页复杂,内存压力大时可能加剧抖动。
可能影响 fork / copy-on-write
某些数据加载或服务启动模式中,如果进程使用 fork,THP 可能让 copy-on-write 的粒度变大,造成额外内存开销。
THP 一定要关闭吗?
不是。更准确的说法是:
> 吞吐型、长时间运行、内存访问模式稳定的任务可能从大页受益;低延迟、强稳定性、容易受内存碎片影响的在线服务通常倾向关闭 THP 或设为 madvise。
常见策略:
always :尽量对所有匿名内存使用 THP
madvise :只有应用显式 madvise 时才使用 THP
never :禁用 THP
在深度学习系统中,可以这样理解:
| 场景 | 常见策略 | 原因 |
|---|---|---|
| 在线推理服务 | never 或 madvise | 更关注 P99/P999 稳定性 |
| 离线训练任务 | benchmark 后决定 | 可能收益来自 TLB miss 降低,也可能收益不明显 |
| 数据库/参数服务 | 通常 never 或 madvise | 避免 compaction 和回收抖动 |
| 大规模 CPU 内存扫描 | 可能受益 | 访问模式稳定、TLB 压力大 |
如果某个训练任务主要瓶颈是 TLB miss 或大规模 CPU 内存扫描,THP 可能有收益;如果主要瓶颈是 GPU 计算或 I/O,THP 收益可能不明显,反而可能带来抖动。
显式 HugeTLB 与 THP
还有一种方式是显式 HugeTLB:
提前预留 huge pages
应用显式使用
行为更可控
| 机制 | 使用方式 | 优点 | 缺点 |
|---|---|---|---|
| 普通 4KB 页 | 默认 | 稳定、灵活 | TLB 覆盖范围小 |
| THP | 内核自动 | 应用无感,可能提升吞吐 | 可能引入延迟抖动 |
| HugeTLB | 显式预留/使用 | 可控、稳定 | 配置复杂,灵活性差 |
和大模型训练/推理的关系
大模型系统里,Linux 内存管理会影响:
- 权重加载时的 page cache 行为;
- mmap 权重文件的 page fault;
- DataLoader worker 的内存分配;
- pinned memory 是否能稳定分配;
- 容器 memory limit 下的 reclaim;
- THP compaction 对 P99 的影响;
- NUMA node 本地内存是否足够;
- fork + copy-on-write 的额外内存开销。
大模型权重加载为什么是系统瓶颈?
大模型权重可能是几十 GB、几百 GB,甚至 TB 级分片。加载路径涉及:
磁盘 / 网络存储
文件系统
page cache
用户态 buffer
反序列化
CPU 内存
pinned memory
GPU HBM
如果路径不合理,GPU 会一直等待权重或 batch 数据。典型问题包括:
- page cache 抖动;
- CPU 拷贝过多;
- 上下文切换频繁;
- 小 I/O 太多;
- 反序列化慢;
- NUMA 不匹配;
- PCIe 拷贝慢。
传统 read/write 路径
以从磁盘读取权重到用户态为例:
read(fd, user_buffer, size);
典型路径是:
至少涉及:
1 次 DMA:磁盘 → 内核 page cache
1 次 CPU copy:page cache → 用户态 buffer
如果之后还要拷贝到 GPU:
整体可以理解为:
磁盘 → page cache → user buffer → GPU HBM
其中:
- 磁盘 → page cache:DMA。
- page cache → user buffer:CPU 拷贝。
- user buffer → GPU HBM:DMA,经 PCIe/NVLink 相关路径。
read/write 的上下文切换
以阻塞 read() 为例,典型过程是:
从用户进程视角,至少有:
用户态 → 内核态
内核态 → 用户态
如果 I/O 阻塞,还会有:
进程调度出去
I/O 完成后再调度回来
对于大量小文件、小 read,系统调用和上下文切换开销会非常明显。
传统 write() 类似:
如果写网络 socket:
User Buffer → Kernel Socket Buffer → NIC DMA → Network
传统路径的主要问题是用户态和内核态之间多次拷贝、系统调用次数多、上下文切换多、page cache 可能污染。
mmap:把文件映射到地址空间
mmap 可以把文件映射到进程虚拟地址空间。
传统 read:
read(fd, user_buffer, size)
mmap:
ptr = mmap(file)
直接访问 ptr[i]
访问路径变成:
相比 read() 的:
磁盘 → page cache → user buffer
mmap() 可以避免:
page cache → user buffer 的显式 CPU 拷贝
因为用户态虚拟地址直接映射到 page cache 对应的物理页。
这对大权重文件有价值:
- 不用一次性 read 到用户 buffer。
- 可以按需 page fault 加载。
- 多个进程可以共享同一份 page cache。
- 减少用户态额外内存副本。
mmap 的代价
mmap 不是万能的,它的问题包括:
- page fault 开销:第一次访问页面时,如果页面不在内存中,会触发 page fault。
- 随机访问可能导致大量缺页:访问模式很随机时可能造成 page fault 风暴。
- 预取策略需要调优:可以配合
madvise。 - 仍然要拷贝到 GPU:mmap 优化文件到 CPU 地址空间,不代表权重自动进入 GPU HBM。
加载模型时仍然可能需要:
mmap file
→ CPU 解析 tensor metadata
→ cudaMemcpyAsync
→ GPU HBM
mmap 适合什么?
适合:
- 大文件;
- 只读权重;
- 多个 worker / 进程共享;
- 按需加载;
- 随机访问部分权重;
- 减少用户态 buffer 副本。
不适合:
- 极端顺序大吞吐且希望绕过 page cache;
- 对 page fault 抖动极敏感;
- 访问模式不可预测;
- 文件生命周期很短。
Direct I/O:绕过 page cache
Direct I/O 通常指使用 O_DIRECT 绕过 page cache。
传统 buffered I/O:
Disk → Page Cache → User Buffer
Direct I/O:
Disk → User Buffer
它主要优化三点:
- 避免 page cache 污染:大模型权重文件巨大,一次加载可能把 page cache 塞满,挤掉其他服务热数据。
- 减少一层缓存管理:不经过 page cache,可以减少内核缓存管理开销。
- 让应用自己控制缓存:高性能推理引擎或存储系统可以自己管理 buffer、预取、对齐和生命周期。
Direct I/O 的代价是要求更严格:
- buffer 地址对齐;
- I/O size 对齐;
- file offset 对齐;
- 通常需要较大的 I/O 粒度;
- 绕过 page cache 后重复读取不会自动命中缓存;
- 应用自己要做缓存;
- 小 I/O 性能可能更差。
适合:
- 大文件顺序读取;
- 应用自己做缓存;
- 不希望污染 page cache;
- 权重只加载一次;
- 存储吞吐很高。
不适合:
- 大量小随机 I/O;
- 希望依赖 page cache 加速重复访问;
- 应用不想处理对齐和 buffer 管理。
sendfile 与零拷贝
sendfile() 用于在两个文件描述符之间传输数据,典型是:
文件 fd → socket fd
传统用户态转发:
read(file_fd, user_buffer)
write(socket_fd, user_buffer)
路径是:
sendfile() 优化后,路径可以接近:
它主要减少:
- 内核态 → 用户态的数据拷贝;
- 用户态 → 内核态的数据拷贝;
- 系统调用次数;
- 上下文切换;
- CPU cache 污染。
适合:
- 文件服务器;
- 模型权重分发服务;
- HTTP 静态文件下载;
- 节点间传输 checkpoint / shard。
sendfile 对 GPU 加载的最终一步不是主要路径。GPU 加载通常是:
Disk → CPU memory/page cache → GPU HBM
而 sendfile 更适合:
Disk/File → Network Socket
所以它更适合模型分发链路,而不是单机内权重进入 GPU HBM 的最终一步。
四种 I/O 方式对比
| 方式 | 数据路径 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| read/write | Disk → Page Cache → User Buffer | 简单、通用、兼容性好 | 多一次用户态拷贝,syscall 开销明显 | 普通文件读取、小中型文件 |
| mmap | Disk → Page Cache → 用户虚拟地址映射 | 少一次用户态拷贝,按需加载,多进程共享 page cache | page fault 抖动,访问模式影响大 | 大权重文件、只读模型、按需访问 |
| Direct I/O | Disk → User Buffer | 绕过 page cache,避免缓存污染,应用可控 | 对齐要求高,小 I/O 不友好,需自管缓存 | 大文件顺序读、高性能存储、一次性加载 |
| sendfile | Disk/Page Cache → Socket/NIC | 避免数据进用户态,减少拷贝和 syscall | 主要用于文件到网络,不适合复杂解析 | 模型分发、文件服务、checkpoint 传输 |
大模型权重加载优化思路
以推理服务加载大模型为例,典型链路是:
优化目标是:
- 减少拷贝;
- 减少 page fault 抖动;
- 提高 I/O 并行度;
- 匹配 NUMA;
- 避免 page cache 污染;
- 让 GPU 尽早拿到可用权重。
常见手段:
相比复杂 pickle 反序列化,结构化、连续、可 mmap 的权重格式更容易做按需加载和并行加载。
- 使用 safetensors 等更易 mmap 的格式。
大模型通常有多个 shard,可以多线程并行读取,但不要超过磁盘队列和 CPU 解码能力,不要造成 page cache 抖动,不要跨 NUMA 搬运数据。
- 并行加载 shard。
CPU 到 GPU 拷贝建议使用 pinned memory,这样 DMA 更高效,也更容易与计算 overlap。
- 使用 pinned memory 加速 H2D。
GPU 0-3 挂 Socket 0,GPU 4-7 挂 Socket 1 时,权重加载线程也应该分组,避免 Socket 1 读入内存再跨 Socket 给 GPU 0。
- 做 NUMA-aware loading。
权重只加载一次,可以考虑 Direct I/O、posix_fadvise(DONTNEED)、madvise(DONTNEED);权重会反复加载或多个进程共享,则 mmap + page cache 可能更合适。
- 控制 page cache。
面试综合回答模板
I/O 方面,传统 read 路径通常是磁盘 DMA 到 page cache,再 CPU copy 到用户 buffer,如果再加载到 GPU,还要从 CPU buffer 拷贝到 GPU HBM。mmap 可以把文件映射到进程地址空间,减少 page cache 到 user buffer 的一次拷贝,但可能引入 page fault 抖动。Direct I/O 绕过 page cache,适合大文件顺序读和不希望污染 page cache 的场景,但需要处理对齐和缓存管理。sendfile 则适合文件到网络 socket 的零拷贝传输,例如模型权重分发,减少数据进入用户态带来的拷贝和上下文切换。对于大模型权重加载,实际优化要结合 mmap/Direct I/O、并行 shard 加载、pinned memory、NUMA-aware loading 和 page cache 控制综合设计。
mmap 适合只读大权重文件、按需加载和多进程共享 page cache;Direct I/O 适合大文件顺序读取、应用自己做缓存且不希望污染 page cache 的场景;sendfile 适合模型权重分发、checkpoint 文件传输或静态文件服务,因为它优化的是文件到 socket 的路径。真正把权重加载进 GPU HBM 时,通常还需要 CPU 侧解析和 H2D 拷贝,sendfile 不是最终一步的主要优化。