3. 内存管理与优化:让机器人的“大脑”不再卡顿

各位工程师,咱们继续。内存管理,说白了就是管好机器人的“短期记忆”。ACT 模型在运行时,需要频繁读写大量数据——图像特征、关节角度、动作序列……一旦内存出问题,轻则掉帧,重则直接崩溃。我见过太多项目,模型精度调得漂漂亮亮,一上真机就卡成 PPT,最后发现是内存泄漏在作祟。

这一章,咱们就聊聊怎么把内存这块“硬骨头”啃下来。我会从三个层面展开:内存使用模式分析内存泄漏排查缓存与虚拟内存调优。嗯,都是实战中摸爬滚打出来的经验。

3.1 分析内存使用模式:先搞清楚“谁在吃内存”

优化之前,你得先知道内存被谁吃了。我个人习惯用 nvidia-smipsutil 打一套组合拳。别小看这些基础工具,它们能帮你快速定位问题。

核心思路: 区分“模型权重”和“运行时数据”。模型权重是静态的,一般固定;运行时数据(比如 replay buffer、中间特征图)才是动态波动的元凶。

举个例子,我曾经排查过一个 ACT 模型在训练时内存持续增长的问题。用 nvidia-smi 一看,显存占用每 10 分钟涨 200MB。这不对劲。于是我用 psutil 写了个小脚本,每 5 秒记录一次进程的内存快照:

import psutil
import time

process = psutil.Process()
memory_samples = []

for _ in range(120):  # 采样 10 分钟
    mem_info = process.memory_full_info()
    memory_samples.append({
        'rss': mem_info.rss / 1024**2,  # 物理内存,MB
        'vms': mem_info.vms / 1024**2,  # 虚拟内存,MB
        'uss': mem_info.uss / 1024**2,  # 独占内存,MB
    })
    time.sleep(5)

# 分析增长趋势
print(f"起始 USS: {memory_samples[0]['uss']:.1f} MB")
print(f"结束 USS: {memory_samples[-1]['uss']:.1f} MB")

结果发现 uss(独占物理内存)在稳步增长,而 vms 增长更快。这说明有对象在持续分配,但没有被释放。嗯,典型的泄漏前兆。

我的小技巧: 在 PyTorch 中,可以用 torch.cuda.memory_summary() 查看显存分配的详细情况。它会告诉你每个张量占了多少显存,哪个操作分配最多。这比瞎猜强多了。

3.2 排查内存泄漏:揪出那个“只吃不吐”的家伙

内存泄漏是 ACT 系统里最隐蔽的敌人。它不会立刻让你崩溃,但会慢慢拖垮整个系统。为什么会这样?说白了,就是有些对象被意外地“引用”住了,垃圾回收器没法清理它们。

我曾经在一个 ACT 推理服务中遇到过:每次执行一个动作序列,内存就涨 50MB,但序列结束后内存不降。排查了两天,最后发现是 Python 的列表引用 在作祟。

看这个典型错误:

class ActionBuffer:
    def __init__(self):
        self.buffer = []  # 这个列表会无限增长
    
    def add_action(self, action):
        # 错误:没有限制 buffer 大小
        self.buffer.append(action)
    
    def get_latest(self):
        return self.buffer[-1]  # 只取最新,但旧数据永远不删

这个 ActionBuffer 只取最新动作,但旧动作一直留在 self.buffer 里。随着时间推移,内存就炸了。修复很简单:

class ActionBuffer:
    def __init__(self, max_size=1000):
        self.buffer = []
        self.max_size = max_size
    
    def add_action(self, action):
        self.buffer.append(action)
        if len(self.buffer) > self.max_size:
            # 弹出最旧的数据
            self.buffer.pop(0)
    
    def get_latest(self):
        return self.buffer[-1] if self.buffer else None

避坑指南: 我曾经在 PyTorch 的 DataLoader 里踩过坑——num_workers 设置太大(比如 16),每个 worker 都会复制一份模型参数,导致内存直接翻 16 倍。后来我限制 num_workers=4,并开启 pin_memory=True,内存占用立刻降下来了。

排查泄漏时,我推荐用 tracemalloc 这个 Python 内置工具。它能追踪每个对象的分配位置:

import tracemalloc

tracemalloc.start()

# ... 运行你的 ACT 推理循环 ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

输出会告诉你:哪个文件、哪一行代码分配了最多内存。嗯,这招屡试不爽。

3.3 优化缓存命中率:让数据“随叫随到”

ACT 模型在推理时,经常需要反复访问同一批数据——比如图像特征、历史动作。如果每次都要从主存(DRAM)里读,速度会慢得离谱。这时候,缓存 就派上用场了。

你想想看,CPU 的 L1 缓存访问延迟是 1ns 左右,而主存是 100ns。差了整整两个数量级。所以,优化缓存命中率,就是让数据尽量留在离计算单元近的地方。

我常用的优化手段有两个:

  • 数据对齐: 把频繁访问的数据结构放在连续的内存地址上。比如,把多个动作序列打包成一个 torch.Tensor,而不是用 Python 列表存。
  • 预取(Prefetching): 在 ACT 推理时,提前把下一帧需要的特征加载到缓存里。PyTorch 的 DataLoader 自带 prefetch_factor 参数,可以设置预取数量。

举个例子,我之前优化过一个 ACT 模型的推理管线。原始代码是逐帧处理:

for frame in video_frames:
    feature = model.extract_feature(frame)  # 每次都要从主存读 frame
    action = model.predict_action(feature)

改成批量预取后:

# 预取 4 帧到连续内存
batch_frames = torch.stack([next(frame_iter) for _ in range(4)]).cuda()
features = model.extract_feature(batch_frames)  # 一次读取,缓存友好

for i in range(4):
    action = model.predict_action(features[i])

就这么一改,推理延迟从 35ms 降到了 22ms。为什么?因为 torch.stack 把数据排成了连续的内存块,CPU 缓存命中率大幅提升。

关键指标:perf stat -e cache-misses,cache-references 可以监控程序的缓存缺失率。如果 cache-misses / cache-references 超过 10%,就该考虑优化了。

3.4 调整虚拟内存参数:给系统一点“弹性空间”

虚拟内存,说白了就是硬盘假装成内存。当物理内存不够时,操作系统会把不常用的数据换到硬盘上。但 ACT 模型对延迟极其敏感——一次缺页中断(page fault)可能让推理延迟飙升到几百毫秒。

所以,我的建议是:尽量别让系统用到虚拟内存。如果物理内存实在不够,那就调整参数,让交换行为更可控。

在 Linux 系统上,有两个关键参数:

参数 默认值 推荐值(ACT 场景) 说明
vm.swappiness 60 10 控制系统倾向于使用交换分区的程度。值越低,越少用交换。
vm.vfs_cache_pressure 100 50 控制系统回收 dentry 和 inode 缓存的倾向。值越低,缓存保留越久。

调整方法很简单:

# 临时调整(重启后失效)
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50

# 永久调整
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf
echo "vm.vfs_cache_pressure=50" | sudo tee -a /etc/sysctl.conf

注意: 别把 vm.swappiness 设成 0。我曾经这么干过,结果系统在内存紧张时直接 OOM(Out Of Memory)杀掉了我的 ACT 进程。设成 10 左右比较稳妥,既避免频繁交换,又留有余地。

另外,如果你用 Docker 部署 ACT 服务,记得设置内存限制:

docker run --memory="4g" --memory-swap="6g" my-act-image

这样容器最多用 4GB 物理内存,6GB 虚拟内存(含交换)。超出限制会被 OOM Kill,但至少不会拖垮宿主机。

3.5 知识体系总览

说了这么多,咱们用一张图来总结本章的核心逻辑。这张图展示了内存优化的三个层次:从分析模式,到排查泄漏,再到缓存和虚拟内存调优。每一步都环环相扣。

ACT 内存管理与优化知识体系 3.1 分析内存使用模式 工具:nvidia-smi, psutil, torch.cuda.memory_summary 目标:区分静态权重 vs 动态运行时数据,定位增长趋势 3.2 排查内存泄漏 工具:tracemalloc, 引用计数分析, 限制 buffer 大小 常见陷阱:无限增长的列表、DataLoader worker 过多 3.3 优化缓存命中率 方法:数据对齐(连续内存)、预取(prefetch_factor) 监控:perf stat -e cache-misses,cache-references 3.4 调整虚拟内存参数 关键参数:vm.swappiness=10, vm.vfs_cache_pressure=50

这张图把咱们讲的内容串起来了。你从最上层的分析开始,一步步往下走,就能把内存问题彻底搞定。记住,优化不是一蹴而就的,而是一个持续迭代的过程。

最后一句心里话: 内存优化这件事,七分靠工具,三分靠经验。多跑几次 tracemalloc,多看看 perf 的输出,慢慢你就能培养出“内存直觉”——看到一段代码,就能大概猜出它会不会泄漏。嗯,这就是资深工程师的底气。


公众号:蓝海资料掘金营,微信deep3321