云计算百科
云计算领域专业知识百科平台

Kestrel_VLLM - 40 · 把 CUDA Graph 默认打开:一条默认值改动的两个前置

40 · 把 CUDA Graph 默认打开:一条默认值改动的两个前置

一句话:hybrid 的 CUDA Graph 早已落地、位级也证过,但它一直默认关 ⇒ 生产上从没开过。把默认值从关改成开看起来是一行的事,实际是「先量 → 拆门 → 默认化 → 全回归」:其中一个自己的推断被先量推翻,还有一道门必须先拆,不拆就会在第一次跑回归时把 –mt 当场打断。 前置:第 29 篇(CUDA Graph 接到 hybrid —— 图化的真正工作在「把随 token 变化的量赶出内核参数」)、第 36 篇(一次退役 —— M 行 graph 为什么停用)。 环境:x86-64 + NVIDIA RTX 5090 · Qwen3.5-27B q4(hybrid)· CUDA 后端(阶段五)。

一、问题与结论

hybrid 的 CUDA Graph 早就落地了,而且位级已经证过(Graph 档 vs 流式档的 h64/hfinal/logits 逐字节相同)。但它一直默认关 ⇒ 生产上从没开过。

「把默认值从关改成开」看起来是一行的事。实际不然:这一轮里,有一个我自己的推断被先量推翻,还有一道门必须先拆 —— 不拆,默认化会在第一次跑回归时把 –mt 当场打断。

结论数值判定
对象 hybrid M=1 步进的 CUDA Graph,门 VLLM_CUDA_Q35_GRAPH ——
先量(零改动探针,N=5/16/64) ≈1.11×,且与 N 无关 实测
自纠 「短 T 要加阈值门」是错的:那是捕获被算进被测窗口 实测
落地收益 ≈1.11×(效应 1.465 / 配对极差 0.038 = 38.6× ≥ 5×) 实测
位级 两档三文件逐字节相同;=0 档与引入前逐字节相同 实测
全回归 8/8 逐项与 _w4g 相同 实测

要点:

  • 对象:VLLM_CUDA_Q35_GRAPH(hybrid M=1 步进的 CUDA Graph)。graph 化的收益与位级此前都已确证,唯一缺的是默认开。
  • 先量(零改动探针):q35_gpu_model –iters N 同会话交错 A/B,N=5/16/64 ⇒ ≈1.11×,且与 N 无关(N=5 也是 1.11×)⇒ 「短 T 净亏、需要阈值门」不成立。
  • ★ 自纠:我曾据「引擎 forward 分支 T=5 时 ON 反而慢 +10.1 ms」推断「短 T 要加阈值门」。复测表明那是捕获被算进了被测窗口(prepare 未 reset),不是 graph 变慢。
  • 先量还顺带订正了一条旧表述:CUDA 路径的「prefill」就是逐 token 循环同一个 model_step,而该 step 早被 graph 捕获(捕获点在 vcuda_dev_model_reset)⇒ 不存在「prefill 未接 graph」这条缺口。
  • 必须先拆门:同一个 VLLM_CUDA_Q35_GRAPH 门同时管着「M=1 步进图」与 M 行 per-M 图;而 vcuda_dev_model_step_m 在 graph 模式下直接 return -1 ⇒ 默认一开,M 行入口默认就拒,–mt 与全部 M 行回归当场失效。做法:拆成 VLLM_CUDA_Q35_GRAPH(M=1,默认开)与 VLLM_CUDA_Q35_GRAPH_M(M 行,默认关)。
  • 落地结果:G1–G5 全过 —— 位级两档逐字节相同、收益 ≈1.11×、关档逐字节回到引入前、门表更新;全回归 8/8 逐项与 _w4g 相同。
  • 审计抓到一个拆门漏项:M 行路径的动态 smem 定尺 latch 也得改判新门,否则它会被 M=1 图默认开静默牵连(详见 3.4)。
  • 二、背景

    graph 的门一旦默认开,最先被问的就是「短序列会不会更亏」——图捕获有固定成本,序列越短,这笔摊得越薄。所以在改默认值之前,先用一个零改动探针把这个问题量掉;量完才发现,真正要先处理的是这个门还管着什么别的。

    三、核心机制与做法

    3.1 先量:短 T 到底亏不亏

    q35_gpu_model –iters N(A = graph 关;B = graph 开;同轮内配对):

    NOFF ms/tokenON ms/token配对省
    5 14.920 / 14.862 13.349 / 13.414 1.571 / 1.448
    16 14.906 / 14.966 13.378 / 13.402 1.528 / 1.564
    64 15.046 / 15.045 13.524 / 13.542 1.522 / 1.503

    ⇒ ≈1.11×,且与 N 无关。N=5 也是 1.11× ⇒ 「短 T 净亏」不成立,阈值门基本不需要。

    ★ 自纠(这一条是本轮最重要的方法论点):我此前据「引擎 forward 分支在 T=5 时 ON 反而慢 ≈10.1 ms」推断要加「按 T 的阈值门」。复测才看清:

    那不是 graph 变慢,而是捕获成本被算进了该路径的被测窗口(prepare 没有 reset)。一旦捕获落在请求窗口之外(比如以 reset 为序列起点),这是一次性的。

    这条后来被写进判据 G3:捕获必须在请求窗口外。

    3.2 为什么默认化之前必须先拆门

    现状:一个门 VLLM_CUDA_Q35_GRAPH 同时管两件事 —— 「M=1 步进图」与 M 行 per-M 图(代码注释:「沿用同一个门」)。

    而 vcuda_dev_model_step_m(M 行入口)在 s->mdl_graph_mode 为真时直接 return -1(批量路径只走流式):

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu —— M 行入口:graph 模式下直接拒绝 */
    VCUDA_API int vcuda_dev_model_step_m(void *dev, int M, int pos,
    const float *h, float *h_out, float *logits) {
    vc_dev_t *s = (vc_dev_t *)dev;
    if (!s || !s->ok || !h) return –1;
    if (M < 2 || M > s->mdl_mmax) return –1; /* 门没开 / M 不匹配 ⇒ 拒绝 */
    /* … */
    if (s->mdl_graph_m_mode) return –1; /* 批量路径只走流式 */
    /* … */
    }

    ⇒ 把默认值一改成「开」,M 行入口默认就拒 —— –mt 与全部 M 行回归当场失效。这不是「可能出问题」,是必然。

    做法(拆门):把两个特性拆成两个独立门,各读各的 env:

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu —— 拆门后的两个 env 读取 */
    /* M=1 的图:§18.40 起**默认开**,只有显式 `0` 才关。 */
    static int vc_mdl_graph_env(void) {
    static int v = –1;
    if (v < 0) {
    const char *e = getenv("VLLM_CUDA_Q35_GRAPH");
    v = (e && e[0] == '0') ? 0 : 1; /* 默认开,=0 才关 */
    }
    return v;
    }
    /* M 行 per-M 图(已退役):**独立门**,**默认关**,只有显式开才开。 */
    static int vc_mdl_graph_m_env(void) {
    static int v = –1;
    if (v < 0) {
    const char *e = getenv("VLLM_CUDA_Q35_GRAPH_M");
    v = (e && e[0] && e[0] != '0') ? 1 : 0; /* 只有显式开才开 */
    }
    return v;
    }

    (上面代码块里的 §18.40 是仓库内部规格编号,读者可忽略;详见仓库 docs/。)

    门管什么默认
    VLLM_CUDA_Q35_GRAPH M=1 步进图 开(=0 才关)
    VLLM_CUDA_Q35_GRAPH_M M 行 per-M 图(已退役) 关(显式开才开)

    vcuda_dev_model_step_m 的拒绝改判新门(新字段 mdl_graph_m_mode)⇒ 新门默认关 ⇒ –mt 行为不变。

    这里的分工很干净:默认化只动 M=1 那条;M 行图(已退役)只把它的门独立出来,不重启它。

    3.3 判据(预登记,写死,防事后改判)

    #判据口径
    G1 位级不变 graph 开/关两档 –dump 的 h64/hfinal/logits 逐字节 memcmp=0;主模型对 _q4ref64 三项 maxrel 逐字不变 + argmax 5/5
    G2 收益可鉴别 同会话交错 A/B(≥3 轮):效应 ≥ 5× 配对极差,预期 ≈1.11×
    G3 捕获在请求窗口外 默认开后首次请求不得因捕获劣化超过一次捕获成本(≈10 ms)
    G4 回退门 =0 ⇒ 与引入前逐字节相同(真 memcmp,不许用「读数相同」代)
    G5 门表更新 门表把本门改「开」并限义为「M=1 步进图」;新增 VLLM_CUDA_Q35_GRAPH_M(默认关)一行

    3.4 审计抓到一个拆门漏项

    拆门不是「把拒绝判据改一下」就完了 —— 任何以 mdl_graph_mode 为 latch 的地方都要重新判。

    漏掉的是 vc_attn_step_dev_m:它按 s->mdl_graph_mode 决定动态 smem 的定尺与 cudaFuncSetAttribute 守卫。若不改,M 行路径会因 M=1 图默认开而静默改用 graph 档定尺,同时把属性守卫变成死代码。这两处的 latch 必须改判 M 行自己的门:

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu —— 现役内核的动态 smem 定尺(改判 M 行门) */
    if (!fa_on) { /* 现役内核:门关 / FA 未启用 / 被门限跳过 */
    const size_t shbytes = s->mdl_graph_m_mode
    ? ((size_t)s->attn_maxseq + (size_t)M + (size_t)hd) * 4 /* graph 档:与 pos 无关 */
    : ((size_t)(len + M) + (size_t)hd) * 4; /* 流式档:按当前 len */
    if (!s->mdl_graph_m_mode && shbytes > 48 * 1024) {
    if (cudaFuncSetAttribute(k_attn_decode,
    cudaFuncAttributeMaxDynamicSharedMemorySize,
    (int)shbytes) != cudaSuccess) {
    cudaGetLastError(); return –1;
    }
    }
    k_attn_decode<<<M * nh, hd, shbytes, stream>>>(mao, mbg, kc, vc, s->d_dyn, /* … */);
    }

    订正:这两处的 latch 由 mdl_graph_mode 改判 mdl_graph_m_mode。

    ★ 附带一个必须记下的前置条件:vc_attn_step_dev_m 里 mdl_graph_m_mode ≡ 0 是载荷不变量,仅由「vcuda_dev_model_step_m 开头 early-return」+「vc_mdl_graph_m_ensure 不可达」共同守护。若将来有人新增一个「开新门却绕过 ensure」的 vc_model_step_body_m 直调者,该处会走 graph 档定尺却跳过 cudaFuncSetAttribute ⇒ shbytes > 48 KB 时可能发射失败。重接那条 M 行 graph 之前必须先补这条。

    同轮还顺带订正了 3 处过期注释(两处「默认关」→「默认开」;G2 比值注释补上「终档 38.6×」),保持行数不变,故台账行号不受影响。

    四、实测数据

    实测(最终档):

    #实测结论
    G1 h64/hfinal/logits 三文件 on-vs-off 全 SequenceEqual=True(102,400 / 102,400 / 4,966,400 B);maxrel 2.2536e-06 / 2.0334e-06 / 3.3887e-06(逐字不变);argmax 5/5 ✅
    G2 –iters 64 3 轮:OFF 14.455/14.454/14.431、ON 12.970/12.991/12.984 ⇒ 配对省 1.485/1.463/1.447 ⇒ 效应 1.465 ms/token、配对极差 0.038 ⇒ 38.6× ≥ 5×;比值 ≈1.11×(与先量探针一致) ✅
    G3 q35_gpu_model 路径默认档首个前向 12.97 ms/token 优于 =0 档 14.80 ms/token ⇒ 该调用方零劣化。★ 口径限定:这只证明「以 reset 为序列起点」的调用方零劣化;引擎 forward/prepare 那条在旧探针里仍测得 ON 比 OFF 慢 ≈10.1 ms(一次捕获成本以内,不超上限,但不是零劣化) ✅(限定口径)
    G4 =0 档 dump 与引入前档(_w4g 默认=流式)三文件逐字节相同;且与更早 build-x64-prev 档亦相同 ✅
    G5 门表已改 + 新增一行(门名 + 默认值 + latch + 验证节) ✅

    拆门的端到端回归(证明「–mt 行为不变」):默认档(graph 开)跑 VLLM_CUDA_Q35_MM=4 –mt 4 ⇒ h_out memcmp=0、logits memcmp=0、等价 PASS、M 行提速 2.325×(与拆门前一致;未被默认开牵连拒绝)。

    全回归 8/8(默认化触及所有 M=1 步进路径,按纪律全跑):

    回归项结果
    P2 主模型零回归 ✅ 三项 maxrel 逐字不变 + argmax 5/5
    W1 负向自检 ✅ 缺键 / 错形状必拒 PASS + argmax 5/5
    W2 逐级对拍 ✅ 设备档 vs f32 参考 PASS
    W3a 回滚自检 ✅ out memcmp=0 + MTP KV 游标回退 PASS
    W3c 判据 2 ✅ token 流 memcmp=0;直方图 0:24/1:6/2:11/3:4 逐格不变
    v0-DLL 优雅退出 ✅ 退出码 1(无 0xC0000005)
    L3 自检 ✅ PASSED (0 failures)
    稀疏自检 ✅ PASSED (9 checks)

    ⇒ 8/8 逐项与 _w4g 相同。

    五、边界与已知限制

    • 收益 ≈1.11× 是同一 harness 内的同会话交错 A/B;绝对值随 GPU 时钟/同会话抖动变化,只作方向判断(引用时必须带日志名)。
    • G3 的「零劣化」只对以 reset 为序列起点的调用方成立;引擎 forward/prepare 那条仍在一次捕获成本(≈10 ms)以内,不是零劣化 —— 口径必须这样写。
    • M 行 graph 本轮不重启:只把它的门独立出来。
    • 与 CPU 的位级一致:有意不做(沿用阶段五口径纪律)。

    CPU 对照(迁移前基线)

    • CPU 参考:CPU 侧无对应实现(CUDA Graph 属 CUDA 后端同期新增)—— CPU 侧即逐 token 步进。
    • 迁移要点:把「一个门管两特性」拆成 VLLM_CUDA_Q35_GRAPH(M=1,默认开)与 VLLM_CUDA_Q35_GRAPH_M(M 行 per-M 图,默认关);所有以旧 latch 为准处逐一改判;捕获须在请求窗口外。
    • 真机验证:命中文中 8/8 回归项中的 E7(–test-l3 PASS(0)、–test-sparse PASS(9));graph 默认开后 E2 于默认档运行。≈1.11× 收益为同会话交错 A/B 读数,未在本机清单复现。

    六、小结

  • 「默认关」本身就是一笔未兑现的收益。一个已落地、已位级证过、收益 1.11× 的开关,因为默认关而在生产里从未生效 —— 这类「差一行」的欠账要专门盘出来。
  • ★ 默认化之前,先看这个门还管着什么别的。本例里,门被两个特性共用;直接默认化会当场打断另一个(–mt)。做法是先拆门、再默认化,并且把所有以旧 latch 为准的地方逐一改判 —— 落下一个,就是审计抓到的静默牵连(见 3.4)。
  • 「推断」要能被「先量」推翻。加阈值门的推断来自被污染的口径(捕获落在计时窗口内);零改动探针一量就现原形 ⇒ 凡是「加个阈值/加个开关」的推断,先做一次零改动 A/B。
  • 回退门要用真 memcmp 验,不许用「读数相同」代替。

  • 相关篇目:第 29 篇(CUDA Graph 接到 hybrid);第 36 篇(一次退役 —— M 行 graph 为什么停用);第 41 篇(prefill 批量化 —— 承接本条,把 M 行路径接到 prefill)。 源码与配套资源:本仓库 https://gitee.com/pei-xiaoguang/kestrel-llm-cuda.git; CPU 推理源码 https://gitee.com/pei-xiaoguang/kestrel-llm

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Kestrel_VLLM - 40 · 把 CUDA Graph 默认打开:一条默认值改动的两个前置
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!