背景

sglang跑Deepseek PD分离,使用mooncake做kv transfer

现象

服务启动后跑个10min都很正常,ttft,tpot都很低,很开心。但是跑了几分钟以后ttft开始增加,mooncake日志里传输速率下降到<100MB/s。

传输慢,P节点的日志里就会看到transferring=10…20…30…越来越多。没传完的请求还占着kv cache的空间,导致新的请求也没法跑,后面的请求都堆积在waiting_queue里面,每次只能运行1个请求。token-usage 0.99。TTFT直线上升。。。。

猜测:计算用ib网络,传kv 也用ib 网络,这两个互相干扰?ib带宽满了?

看PCIE带宽,只占了500MB/s,远远不到IB网络的400Gbps。

把overlap-schedule关掉,先算,算完就把这批请求都传走,让scheduler sleep等到都传完了再跑下一个batch总可以吧

并不,传的还是很慢。

猜测:传输任务提交的慢?

scheduler 跑完以后是调了mooncake 的transfer_sync,这个sync并不是等传完,而是类似提交任务,python这边很快就释放出来了不会hang在这里。mooncake 里面也释放了GIL了。提交任务是用一个多线程提交的,那我增加这个线程池呢?我从4改成64

有点用,传的是快了,仅限前面10分钟,可能原来传500MB/s 的可以变成 700MB/s,但是这10分钟后还是速度掉回几十MB

猜测:传输的内存不连续,带宽利用率低

在每次传的时候都打出来块的大小,发现前面的num-block都是1,2这样,很少,传的很快,但是10分钟后就变成1000+了。这么多碎片内存当然慢了。

分析:这么多碎片哪里来的呢?

前面调试写的sleep还没删,P节点每次都是等传完老的再跑新的,我的kvcache干干净净啊。凭啥搞几千个block出来啊。

看看TokenToKVPoolAllocator代码

1
2
3
4
5
6
7
8
9
10
11
def alloc(self, need_size: int):
if need_size > len(self.free_pages):
return None
select_index = self.free_pages[:need_size]
self.free_pages = self.free_pages[need_size:]
return select_index

def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
self.free_pages = torch.cat((self.free_pages, free_index))

Alloc 直接从free队列里拿 size 个
Free的时候直接加到后面
这显然是有问题的
mem pool frag
只要循环多次,pool的内存排序会收敛于一种完全shuffle的状态。每次申请的block 都是极其不连续的,在page size 1 的情况下会大概率出现 1000个token 在 1000个不连续的block上。这样对于KV传输非常不友好。传输速率只有50~250MB/s

那我free后把free_pages 从小到大排个序不就好了,下次拿都是连续的。

P节点确实都连续了,但是KV transfer的时候P和D的block数是一样的,P连续的但是D不连续啊,就算P 的pool每次都是新的,但D那边还有没跑完的请求没法释放没法弄出来很多连续的block

解决

简单点,直接page-size=128

使用PagedTokenToKVPoolAllocator ,根据实际数据分布设置合适的page size,让大多数请求只需要几个page,这样大大提升请求token的block连续性。 设置page size =128 的情况下,对于4000 以下的真实输入场景,每次KV传输时间<0.2s,Mooncake传输速率保持1500MB/s

后续

transfer很快,但是跑半个小时decode又挂了。查不出问题,只是看到从开始跑linux kernel的内核态内存就一直增长,涨半个小时到阈值不给申请了。然后mooncake里面调syscall ibv_create_qp的时候linux kernel直接返回12 cannot allocate memory

查了一圈找到了一个issueMooncake Issue 691