K3.1 Pro RSI

4 min read

K3.1 Pro RSI

https://github.com/pegainfer-project/kern-k31

WIP: 将 kern 优化好的 kernel 直接在 你的 vllm/sglang 中使用,pip install 即可,我们会自动 plugin 到 forward 上。k3 已经支持,k3.1 主要看这两引擎怎么写的代码。

我们 day0 的是个 repo,kern 主体并不需要改什么,kern-k31 就放 k31 相关的 kernel,并行策略这些。然后整体优化还是用的 kern 的这套 harness。

昨晚上发的,简单支持了个 day0(给了他 vllm sglang的 pr),然后挂了一个 loop,跑了大概一整天,他自己说没啥能干的了。

优化的目标是K3.1 的 decode 共 8 卡,TP8,无投机解码。每个请求的上下文是 128k。分数是 16、32、64、128、256 行五档 TPOT 中位数的加权平均,权重依次是 1、2、4、4、2,线上批大小最常落在 64 和 128 行,所以这两档权重最高。

优化的 Loop 呢,大体就是我挂一个 claude code 的,prompt 类似这样,/loop 20m 优化 K3.1 Pro 的 deocde 性能,尽量用 sub agent优化,你只负责编排他们,以及决定最后是否合入,xxx。

每个优化一个 commit,

  • bit 相等的,直接走 kern bench 看分数
  • 不相等的,走 kern test 看精度是否合理,过了才能走 kern bench 打分

bench 的输入是真实文本,随机 token 会让路由挤到少数专家上,64 行一步少算约 3 ms,分数虚低

分数从 30.52 ms 降到 27.20 ms,快了 10%,每一档都变快。同样 8 卡、每个请求128k,比 SGLang 的官方配方最多快 1.3 倍;BS=256 时 SGLang 显存放不下。

单项收益最大的是把专家执行改成 EP2×TP4,省 0.86 ms,省下的是各 rank 等最慢那个 rank 的时间。其余合入的改动每项省 0.1 到 0.7 ms,集中在 DCP 的 top-k、DSA 链路和 MoE front 三处。

所以 Kern 是 Attn TP8 + CP8, MoE 部分是 EP2 + TP4.

继续抠单个 kernel 的空间已经不大。128 行的一步里,MoE 专家 GEMM 占一半以上,它读专家权重的速度已经到 HBM 带宽;剩下的每一项都只有 0.1 到 0.5 ms。

K3.1 Pro RSI 优化结果图 1

K3.1 Pro RSI 优化结果图 2

K3.1 Pro RSI 优化结果图 3

具体的优化旅程

分数变化是对当时 main 的差值,负数表示变快。

阶段一:只有 16、32、64 行三档(分数和后面不可比)

#尝试分数变化(ms)结果
1bench 输入从随机 token 换成真实文本20.69 → 23.37采纳。之前的分数偏低
2KDA core 按 PDL 启动+0.05合入,噪声内
3MLA 链按 PDL 启动−0.08合入
4KDA core 的 block 常驻−0.06合入
5top-k 只建一次直方图−0.34合入
6支持 128 和 256 行—合入。分数改成五档,起点 30.52

阶段二:五档分数

#尝试分数变化(ms)结果
7cuBLAS 固定算法—放弃。默认启发式已经是候选里最快的
8MoE FC1、FC2 换 kernel 变体(12 个)0 到 +7.9放弃。没有一个比默认快
9moe_front 改成一条 TMA 链+0.21放弃。单测快 20%,放进整步反而慢
10自写 dense GEMM估算 −0.4没做。收益抵不上维护成本
11DCP 的 v-up 挪到通信之前−0.37合入。求和顺序变,过门
12静态 EPLB估算最忙 rank 的 MoE 负载 −10%没做。不采用离线固定的专家放置
13DCP top-k 候选:压成一个 32 位字、调度只做一次、先发 load、12 位基数−0.72合入
14absorb 挪到 pack 和 select 之间0.00放弃
15shared expert 的 down 在 96 行以上挪到 FC2 之后−0.13合入
16shared down 用自写的尾 kernel+0.19放弃。输给 cuBLAS
17moe_front 每个 SM 放两个 block+0.19放弃
18lat_down 按 8 路切分,在 moe_front 里收齐−0.27合入
19all-reduce 只重置自己上次写过的位置−0.21合入
20all-reduce 成对 epilogue−0.02放弃。只有 256 行受益,要加约 120 行代码且有寄存器溢出
21all-reduce 收齐之后再重置单测变慢放弃
22all-reduce 的 epilogue 只在 owner rank 做—放弃。gather 已经用满链路
23o_proj 的 epilogue 直接写进 all-reduce—放弃。cuBLAS 会写出 −0.0,和“数据未到”的标记冲突
24KDA core 换四种线程映射全部更慢放弃
25DSA 链:hot-384 候选合并、idx_kw 并进 wfu、absorb 从 65 行起用 tcgen05、idx_key 并进 mla_prep−0.50合入。其中一项改了求和顺序,过门
26pack 和 select 合成一次 launch0.00放弃
27lat_up 每卡只存 1/8 的列,最后 gather−0.09合入。每卡省 4.1 GB 权重
28sparse MLA 不再按 PDL 启动0.00合入。它在 wait 之前读了上游的输出,lanes 会让这个竞争暴露出来
29PDL 审计,删掉 ar_finalize 里没用到的 early 分支0.00合入。wait 之前的 55 处读取都查过
30专家改成 EP2×TP4−0.86合入。求和顺序变,过门
31lanes:q_b 和 absorb 放到第二条流,和 indexer 并行−0.18等 kern#112 合入
32front 的 f32 GEMM 去掉 split-K reduce—放弃。自写 kernel 在 32 行以上更慢

128 行一步里还剩什么(rank 0,ms)

部分时间
MoE FC1 + FC217.2
dense cuBLAS 投影8.5
ar_finalize(MoE 跨 rank 求和)1.9
attention 侧 all-reduce1.7
KDA core1.7
indexer logits1.4
moe_front1.2

和 SGLang 的对照怎么比的

SGLang 用 K3.1 的官方镜像跑了两种配置:官方配方 TP8 + DCP8(MoE 走 TP8),以及再加 EP8。权重、fp8 KV、每行 128k、decode 走 CUDA graph 这几点和 kern 一致,两边都不开投机解码。官方配方的 KDA 状态是 fp32,EP8 配置改成了和 kern 一样的 bf16,profile 里两者的 KDA 耗时相同。SGLang 的 TPOT 取服务端 decode 日志里批大小恰好等于目标值的区间,取中位数。