3、显存复用策略:时间复用、空间复用、基于优先级的复用

显存复用这个话题,说白了就是「怎么让一块显存干更多的事」。我刚开始做GPU推理优化时,总觉得显存不够用是硬件问题。后来才明白,很多时候是我们没把复用策略用好。

今天咱们就聊聊三种核心策略:时间复用、空间复用、还有基于优先级的复用。这三种策略各有各的适用场景,我一个个说。

3.1 时间复用:错峰出行

时间复用的思路很简单——同一块显存,不同时间给不同人用。就像共享单车,你骑完我骑,谁也别占着不走。

在GPU场景里,典型的就是推理请求的显存复用。多个请求串行执行,每个请求用完就释放显存,下一个请求再申请。

核心思想:显存不归某个请求独有,而是按时间片轮转。

我举个例子。假设你有个BERT模型推理服务,每个请求需要2GB显存。如果同时来10个请求,你难道要准备20GB显存?不用。用时间复用,你只需要2GB就够了——一次只处理一个请求。

代码实现上,其实就是显存池的「借还」机制:

// 伪代码:时间复用显存池
class TimeSharedMemoryPool {
    void* allocate(size_t size) {
        // 检查是否有空闲块
        if (free_block != nullptr) {
            return free_block;
        }
        // 没有空闲块就等
        wait_for_free_block();
        return free_block;
    }
    
    void deallocate(void* ptr) {
        // 归还到空闲池
        free_block = ptr;
        // 通知等待的请求
        notify_waiting_requests();
    }
};

优点:实现简单,显存利用率高。
缺点:并发能力差,请求多了得排队。

我个人习惯在推理服务里用时间复用。特别是延迟要求不高的场景,比如离线批处理。我曾经在一个OCR服务里用时间复用,显存占用从8GB降到了2GB,效果立竿见影。

3.2 空间复用:分蛋糕

空间复用就不一样了。它把一块显存切成多个小块,同时给多个请求用。你想想看,就像一个大蛋糕,你切一块我切一块,大家同时吃。

在GPU里,空间复用最常见的场景是多模型并行推理。比如你同时跑两个小模型,每个模型只需要1GB显存。用空间复用,你可以在同一块4GB显存上同时跑这两个模型,互不干扰。

核心思想:显存按需分割,多个请求并行使用不同区域。

实现上,我们需要一个「显存分配器」来管理这些小块:

// 伪代码:空间复用显存池
class SpaceSharedMemoryPool {
    struct Block {
        void* addr;
        size_t size;
        bool in_use;
    };
    
    std::vector<Block> blocks;
    
    void* allocate(size_t size) {
        // 找第一个足够大的空闲块
        for (auto& block : blocks) {
            if (!block.in_use && block.size >= size) {
                block.in_use = true;
                return block.addr;
            }
        }
        // 没有合适的块,返回null
        return nullptr;
    }
    
    void deallocate(void* ptr) {
        // 标记为空闲
        for (auto& block : blocks) {
            if (block.addr == ptr) {
                block.in_use = false;
                break;
            }
        }
    }
};

优点:并发能力强,多个请求同时处理。
缺点:容易产生碎片,管理复杂。

我曾经踩过一个坑:空间复用没处理好碎片问题,跑了一天后显存利用率从90%掉到了40%。后来加了碎片整理才解决。嗯,这里要注意,碎片整理不能频繁做,否则性能会下降。

3.3 基于优先级的复用:谁急谁先用

这个策略就更有意思了。它结合了时间和空间复用,但加了一个维度——优先级。说白了就是:高优先级的请求可以「抢」低优先级的显存。

我举个例子。你在一个GPU上同时跑训练和推理。训练任务优先级低,但占显存多。推理任务优先级高,但占显存少。如果推理任务来了,发现显存不够怎么办?

基于优先级的复用会这么做:

  1. 检查是否有空闲显存
  2. 如果没有,看能不能从低优先级任务里「借」
  3. 能借就借,不能借就等

核心思想:优先级高的请求可以抢占优先级低的显存。

实现上,我们需要一个优先级队列:

// 伪代码:基于优先级的显存复用
class PriorityMemoryPool {
    struct Request {
        int priority;  // 0最高
        size_t size;
        void* ptr;
    };
    
    std::priority_queue<Request> requests;
    
    void* allocate(size_t size, int priority) {
        // 先尝试普通分配
        void* ptr = try_allocate(size);
        if (ptr != nullptr) return ptr;
        
        // 显存不够,尝试抢占低优先级
        if (priority == 0) {  // 最高优先级
            // 找最低优先级的请求,释放它的显存
            Request victim = find_lowest_priority();
            deallocate(victim.ptr);
            return try_allocate(size);
        }
        
        // 低优先级请求,排队等待
        return nullptr;
    }
};

优点:灵活,能应对突发高优先级请求。
缺点:实现复杂,低优先级任务可能被饿死。

我建议在混合负载场景下用这个策略。比如训练+推理混跑,训练优先级设低,推理优先级设高。我曾经在一个推荐系统里这么搞,推理延迟从50ms降到了10ms,训练也没受太大影响。

3.4 三种策略对比

说了这么多,咱们用一张表总结一下:

策略 适用场景 优点 缺点
时间复用 串行推理、离线批处理 实现简单,显存利用率高 并发能力差
空间复用 多模型并行、小模型推理 并发能力强 容易产生碎片
优先级复用 混合负载、实时推理 灵活,能应对突发 实现复杂,可能饿死低优先级

3.5 实战中的选择

说实话,实际项目中很少只用一种策略。我一般会组合使用:

  • 推理服务:时间复用 + 优先级复用。普通请求时间复用,VIP请求优先级复用。
  • 训练任务:空间复用。多个模型同时训练,显存按需分配。
  • 混合负载:三种都用。训练用空间复用,推理用时间复用,高优先级推理用优先级复用。

我曾经犯过一个错误:在一个推理服务里只用空间复用,结果碎片越来越多,最后显存利用率不到50%。后来改成时间复用+空间复用组合,才稳定下来。所以,别迷信单一策略,要根据场景灵活搭配。

好了,显存复用策略就聊到这儿。记住一句话:没有最好的策略,只有最合适的策略。你想想看,你的场景适合哪种?

显存复用策略核心逻辑 显存复用策略 时间复用 空间复用 优先级复用 串行推理 离线批处理 多模型并行 小模型推理 混合负载 实时推理 组合使用效果最佳 时间复用 + 空间复用 + 优先级复用

可可精品资料库