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

Kestrel_VLLM - 18 · 投机解码位级对拍收口:batch 与 M=1 一致,注意力路径与 CPU 逐字节对齐

18 · 投机解码位级对拍收口:batch 与 M=1 一致,注意力路径与 CPU 逐字节对齐

一句话:把「M>1 batch verify 与 M=1 decode」的对拍从 top-1 下沉到完整 logits 逐字节,纠正一处 64-token 长序列的误读,并把 CPU↔GPU 的分歧一路消到 layer 0 的 MoE FFN 为止——层 0 整条注意力路径(RMSNorm → QKV GEMM → QK-norm → RoPE → KV 量化 → attention → O 投影+残差)已与 CPU 逐位全等。 前置:建议先读 第 16 篇(batch verify 的 top-1 位级一致)。 环境:x86-64 + NVIDIA RTX 5090(CUDA 后端)· Qwen3-30B-A3B q4local · 开关 VLLM_CUDA_ATTN_BITEXACT=1 / VLLM_CUDA_L2EXACT=1(位级档,opt-in)。

一、问题与结论

  • batch verify 与 M=1 位级一致成立:ISODIFF 探针(完整 logits 按 uint32 逐字节比)全列 ndiff=0,覆盖 pos 33–38。
  • 「token 58 翻转」是误读:纯 CPU 参考 token58=101128,与 GPU 输出一致;此前当作「参考」的 81167 其实是 token21。
  • 唯一真实分歧在 t=27(GPU=1773 vs CPU=3837),非 spec 引入——纯 GPU greedy 同样 1773;隔离对照定位到 CUDA attention。
  • 该分歧已彻底消除:把 attention 的算子顺序对齐 CPU + 位级复刻 UCRT 的 expf 后,CPU↔CUDA 逐 token 位级一致(64/64)。位级路径 opt-in(VLLM_CUDA_ATTN_BITEXACT=1),代价 2.25×。
  • L2 完整模型 harness(逐层 hidden + 完整 logits + KV 行哈希)已可用:CPU 与 CUDA 两侧同一批 key 的 FNV-1a 64 哈希(VLLM_L2_DUMP),2120 key 一次跑完,diff 即得首个差异点。
  • 全 GPU decode 的层 0 注意力路径已逐位全等:在 VLLM_CUDA_L2EXACT=1 档下,_nrm / _kraw / _knorm / _k / _ks / _v / _vs / _qrope / _attn / _ores 十个探针全部与纯 CPU 参考逐字节一致(含 RoPE 的 cosf/sinf 与 attention 的 expf,均已位级复刻 UCRT)。唯一剩项是 layer 0 的 MoE FFN 输出 _x(见第五节,已在 第 22 篇 收口)。
  • 状态:注意力路径 ✅ 位级全等(opt-in 档);MoE FFN 在本篇仍 ❌ 未对齐(已登记,下一篇收口)。

    二、背景:为什么 top-1 对拍不够

    第 16 篇 只对拍了 top-1([SPECTOP1]/[ISOTOP1])。top-1 相同不构成位级证明:仓库方法论的「位级门禁」原则说得直白——两个 logits 可能在 top-1 之外有 ULP 差,只在边界 token 处翻 top-1。于是本篇加 [ISODIFF] 探针:batch 列 j 的完整 logits 与「同 token 顺序 M=1 decode」逐 uint32 字节比,报第一个非零 ULP 差的位置。

    [SPECTOP1] pos=33 col=0 top1=99692 [ISOTOP1] col=0 99692 [ISODIFF] col=0 ndiff=0
    [SPECTOP1] pos=33 col=1 top1=99371 [ISOTOP1] col=1 99371 [ISODIFF] col=1 ndiff=0
    …(pos 33–38,col 0–4 全 ndiff=0)

    → batch 列与顺序 M=1 的完整 logits 逐字节相同,位级一致成立。

    三、核心机制

    3.1 误读纠正:token 58 不是翻转

    实测:

    位置纯 CPU 参考(cpuref64)GPU(spec)判定
    token21 81167 81167 ✓
    token58 101128 101128 ✓

    「参考 81167」是 token21 的值。token58 两边一致,不存在翻转。

    3.2 唯一真实分歧:t=27,源在 CUDA attention

    实测:

    位置纯 CPU 参考GPU(spec / 纯 greedy)
    t=26 33071 33071
    t=27 3837 1773
    t=28 87267 87267

    隔离对照(MoE/lm_head 全留 CPU,实测):

    配置t=27说明
    纯 CPU 3837 基线
    VLLM_CUDA_ATTN=1(仅 attention 上 GPU) 1773 复现分歧
    VLLM_CUDA_INFER=1(仅 GEMM 上 GPU) —(t=24 已先发散) 另一条路径

    → 分歧源是 CUDA attention,不是 MoE GEMM,也不是 batch verify。

    对拍口径上的判定:

    • batch verify vs M=1(同为 CUDA):ndiff=0,位级一致——投机正确性红线不破。
    • CPU↔GPU:本篇之前是 greedy 级(跨硬件 ULP 差)。本篇把它提升到位级(opt-in)。

    3.3 消除 t=27:逆向 UCRT 的 expf

    第一步:算子顺序对齐(不够)。 既有 k_attn_decode_f32q 是两遍 softmax(全局 max → exp → 并行归约求和),归约树求和序与 CPU 的 online softmax 不同。新增 k_attn_decode_online,逐字复刻 CPU 的 flash_attn_single_q_q8_neon:int8 query 量化 + int32 QK 点积 + 逐 token online softmax + 顺序 PV,每 head 一 block、thread 0 串行扫整条 KV。

    用 device expf 时反而更差(实测,t=24 就翻):

    配置首次翻 top-1token
    两遍 softmax(默认 fast) t=27 1773(CPU 3837)
    online 镜像 + device expf t=24 106708(CPU 9370)

    探针:量化 expf 差异(写对拍探针,400002 组 [-100,0] 含随机/密集近 0,实测):

    对比差异率
    CUDA device expf vs UCRT expf(引擎经 api-ms-win-crt-math 走 UCRT) 28.3%(max 2 ULP)
    CUDA device exp()(double) vs UCRT exp()(double) 0 / 400002(逐位一致)
    (float)exp(double) vs 正确舍入(float) 100%
    UCRT expf vs 正确舍入(float) 99.993%(28 例低 1 ULP)

    结论:CUDA 的 expf 与 UCRT 差很多;(float)exp(double)(= 正确舍入)能把差异从 28.3% 压到 0.007%(换成它后 64 token 一致数从 26 提到 56,分歧从 t=24 推到 t=57)。但 UCRT 的 expf 本身不是正确舍入(28 例差 1 ULP),且「到舍入边界的距离」在 bad/ok 之间不可分离——它不是「double 结果加噪声」,而是独立的 float 例程。

    第二步:反汇编 ucrtbase.dll,位级复刻。 用 objdump + dumpbin 定位 ucrtbase.dll 的 expf(RVA 0xA84F0,SSE2 路径),读出算法与常量:

    t = x*K; n = rint(t); j = n&63; m = n>>6;
    r = fma(-n, C2, x); z = r*r;
    p = fma(fma(r,C0,C1), z, r);
    res = 2^m * fma(T[j], p, T[j]); return (float)res;
    K = 64/ln2, C2 = ln2/64, C0 = 1/6, C1 = 1/2, T[j] = 2^(j/64)(64 项表)

    即「3 项泰勒 + 64 项表」,内层全 double + 显式 FMA——所以它的相对误差 ~r⁴/24 ≈ 3.6e-11,这也解释了它为何「几乎正确舍入但差 28 例」。复刻要点(device):__double2int_rn(对应 vcvtpd2dq)、__fma_rn(对应 vfmadd213sd/vfnmadd231sd;-fmad=false 只禁自动融合,显式 FMA 不受影响)、__longlong_as_double 造 2^m,64 项表按位型内嵌。特殊值仅在 x<-104/x>88.72 退化。

    仓库里落地后的 device 复刻(src/npu/cuda/vllm_cuda_kernels.cu,64 项 EXPF_T 表略):

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu */
    /* UCRT expf 的位级复刻。逆向自 C:\\Windows\\System32\\ucrtbase.dll 的 expf
    * (RVA 0xA84F0,SSE2 路径)。host 复刻对 400002 组 [-100,0] 样本与 UCRT
    * expf 逐位一致(0 差异)。 */

    __device__ __forceinline__ float expf_ucrt(float xf) {
    if (xf > 88.7228394f) return (float)exp((double)xf); /* 上溢 -> +inf */
    if (xf < –104.0f) return 0.0f; /* 深度下溢 */
    const double K = 9.23324826168936567683e+01; /* 64/ln2 */
    const double C2 = 1.08304246962491450973e-02; /* ln2/64 */
    const double C0 = 1.66666666666666657415e-01; /* 1/6 */
    const double C1 = 5.00000000000000000000e-01; /* 1/2 */
    double x = (double)xf;
    int n = __double2int_rn(x * K); /* 对应 vcvtpd2dq(就近偶数) */
    double dn = (double)n;
    double r = __fma_rn(–dn, C2, x); /* 对应 vfnmadd231sd */
    int j = n & 63;
    int m = n >> 6;
    double z = r * r;
    double t1 = __fma_rn(r, C0, C1);
    double p = __fma_rn(t1, z, r);
    double Tj = __longlong_as_double((long long)EXPF_T[j]);
    double q = __fma_rn(Tj, p, Tj);
    double two_m = __longlong_as_double((long long)(unsigned long long)(m + 0x3ff) << 52);
    return (float)(two_m * q);
    }

    结果(实测):

    配置与 CPU 逐 token 一致唯一分歧
    默认 fast(两遍 softmax) 63/64 t=27
    VLLM_CUDA_ATTN_BITEXACT=1(online + expf_ucrt) 64/64 无

    解码文本也与 CPU 完全相同。代价:ctx=1024 下 89 → 200 ms/tok(2.25×,double exp + 每 head 串行扫描)。按仓库方法论的默认化门禁(需「位级等价 且 性能领先」),位级路径性能不领先,故不默认化,保留为 opt-in:默认仍走原两遍 softmax(greedy 级,与改动前逐 token 一致)。

    3.4 L2 完整模型对拍 harness

    前面几节把 attention 做到位级,但那不是 L2。按仓库方法论的 L2 口径,L2 = 逐层 hidden + 完整 logits + KV 行哈希全等。

    harness(两侧同 key、同常数):VLLM_L2_DUMP=<path>(默认关):对缓冲求 FNV-1a 64,按 <tag> <hash16> 追加写;CPU 与 CUDA 两侧使用同一批 key,diff 文本即得「第一个差异落在哪层哪步」。CPU 侧在 st_qwen_model_forward 直接算;GPU 侧在 vc_decode_body 用单线程 FNV 内核写到设备暂存、token 末一次性 D2H(避免逐层同步)。

    两侧同常数的哈希实现(src/model/vllm_safetensors.c 与 src/npu/cuda/vllm_cuda_kernels.cu):

    /* 文件名:src/model/vllm_safetensors.c */
    static uint64_t l2_hash64(const void *p, size_t len) {
    const uint8_t *b = (const uint8_t *)p;
    uint64_t h = 1469598103934665603ull;
    for (size_t i = 0; i < len; i++) { h ^= b[i]; h *= 1099511628211ull; }
    return h;
    }
    void st_l2_dump(const char *tag, const void *p, size_t bytes) {
    const char *path = getenv("VLLM_L2_DUMP");
    if (!path || !path[0] || !p || !bytes) return;
    FILE *f = fopen(path, "a");
    if (!f) return;
    fprintf(f, "%s %016llx\\n", tag, (unsigned long long)l2_hash64(p, bytes));
    fclose(f);
    }

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu,设备侧同常数 FNV-1a 64 */
    #define L2_FNV_OFF 1469598103934665603ull
    #define L2_FNV_PRM 1099511628211ull
    __global__ void k_l2_hash(const unsigned char *__restrict p, int nbytes,
    unsigned long long *__restrict out) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;
    unsigned long long h = L2_FNV_OFF;
    for (int i = 0; i < nbytes; i++) { h ^= (unsigned long long)p[i]; h *= L2_FNV_PRM; }
    *out = h;
    }

    每个 decode token 的 key 集合(48 层 → 每 token 530 key):

    key含义
    t<pos>_l<L>_nrm attn RMSNorm 输出(QKV GEMM 输入)
    t<pos>_l<L>_kraw / _knorm K 段投影输出 / K 经 QK-norm(RoPE 前)
    t<pos>_l<L>_qrope Q 经 RoPE(attention 的 Q 输入)
    t<pos>_l<L>_k / _v 本 token 写入的 int8 K / V 行
    t<pos>_l<L>_ks / _vs per-head K / V scale
    t<pos>_l<L>_attn attention 输出(O 投影前)
    t<pos>_l<L>_ores O 投影 + 残差后(FFN 前)的 hidden
    t<pos>_l<L>_x 该层 residual 后的 hidden(FFN 之后)
    t<pos>_final_normed 最终 RMSNorm 输出(lm_head 输入)
    t<pos>_logits 完整 logits(vocab 个 float)

    3.5 前提:对拍配置本身必须干净(VLLM_CUDA_FORCE_CPU=1)

    这是本轮踩过的最大的坑。 全 GPU decode 需要 VLLM_CUDA_INFER=1(把权重预载驻留),但该开关同时也会让 prefill 的 GEMM 走 GPU offload——于是 prefill 写入 KV cache 的 K/V 行与 scale 就已经和纯 CPU 参考差 ~1 ULP。此时无论 decode 侧多干净,L2 都必然不等,且首个差异会伪装成 t32_l0_ks(看起来像 RoPE/attention 的问题),把定位带偏。

    正解:加 VLLM_CUDA_FORCE_CPU=1——设备仍初始化、权重仍预载,但所有 host 侧 offload 回退 CPU(prefill 因此纯 CPU);而 decode 走的是设备内的 vc_decode_body,不受该开关影响,仍全 GPU。位级对拍的正确命令:

    # 参考:纯 CPU
    VLLM_L2_DUMP=ref.txt vllm_kestrel_x64 –model <m> –stream-test –stream-n 4
    # 被测:全 GPU decode + 位级档(prefill 留 CPU)
    VLLM_L2_DUMP=gpu.txt VLLM_CUDA_DECODE=1 VLLM_CUDA_INFER=1 \\
    VLLM_CUDA_FORCE_CPU=1 VLLM_CUDA_L2EXACT=1 \\
    vllm_kestrel_x64 –cuda –model <m> –stream-test –stream-n 4

    3.6 逐项消差(VLLM_CUDA_L2EXACT=1 档)

    按 harness 钉出的顺序一个个改,每改一次复跑、看首个差异是否后移:

    #环节原差异处置
    1 attn RMSNorm / QK-norm GPU 用 rsqrtf(差 34.9%)+ 树形归约 新增 k_rms_norm_cpu/k_qk_norm_cpu:1.0f/sqrtf(与 UCRT 0/400001 逐位一致)+ 逐字复刻 CPU 的 8 元素分块 hsum8 树 / 逐元素升序归约
    2 QKV 投影 GEMM _kraw 起就不同 CPU q4_dense4_x86 在每个 32 块内按 (e, e+16) 配对累加(s += w[e]·x[e] + w[e+16]·x[e+16]),GPU 是严格 0…31。给 k_gw_gemm_f32act 加 pairwise 归约,并强制 nsplit=1(z 分区的块和归约会重排相加序)
    3 RoPE 的 cosf/sinf _qrope 差 反汇编 ucrtbase.dll 位级复刻(见 3.6.1)
    4 decode attention _attn 差 接上已有的 k_attn_decode_online(online softmax + 顺序 PV + expf_ucrt),并修正可见 token 数口径(dyn[1]+1,CPU 侧为自增后的 cache_len[l])

    仓库里这两个「同序」内核(src/npu/cuda/vllm_cuda_kernels.cu):

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu,k_rms_norm_cpu / k_qk_norm_cpu */
    __global__ void k_rms_norm_cpu(float *__restrict out, const float *__restrict x,
    const float *__restrict w, int d, float eps) {
    __shared__ float rd[1];
    const int i = threadIdx.x;
    if (i == 0) {
    float ss = 0.0f;
    const int d8 = d & ~7;
    for (int b = 0; b < d8; b += 8) { /* CPU 的 8 元素分块 hsum8 树 */
    float lo[4], hi[4];
    for (int k = 0; k < 4; k++) { lo[k] = x[b+k]*x[b+k]; hi[k] = x[b+4+k]*x[b+4+k]; }
    for (int k = 0; k < 4; k++) lo[k] += hi[k];
    const float s01 = lo[0] + lo[1], s23 = lo[2] + lo[3];
    ss += s01 + s23;
    }
    for (int j = d8; j < d; j++) ss += x[j] * x[j];
    rd[0] = 1.0f / sqrtf(ss / (float)d + eps); /* 与 UCRT 逐位一致;GPU rsqrtf 差 34.9% */
    }
    __syncthreads();
    const float rms = rd[0];
    for (int j = i; j < d; j += DC_BLK) out[j] = x[j] * rms * w[j];
    }
    __global__ void k_qk_norm_cpu(float *__restrict x, const float *__restrict w,
    int hd, int nheads) {
    const int h = blockIdx.x;
    if (h >= nheads) return;
    float *xh = x + (size_t)h * hd;
    __shared__ float rd[1];
    const int i = threadIdx.x;
    if (i == 0) {
    float ss = 0.0f;
    for (int j = 0; j < hd; j++) ss += xh[j] * xh[j]; /* 逐元素升序 ss += x[i]^2 */
    rd[0] = 1.0f / sqrtf(ss / (float)hd + 1e-6f);
    }
    __syncthreads();
    const float rms = rd[0];
    for (int j = i; j < hd; j += DC_BLK) xh[j] = xh[j] * rms * w[j];
    }

    结果:层 0 内每加一个探针就把范围缩小一次——_nrm ok 后从 _kraw 继续,_kraw ok 后再看 _qrope,_qrope ok 后再看 _attn,最后只剩 _x。

    3.6.1 UCRT cosf/sinf 的反汇编复刻

    与 3.3 的 expf 同一套方法。GetProcAddress(ucrtbase) 定 RVA:cosf = 0xa7d20、sinf = 0xac110(expf = 0xa84f0,与 3.3 一致,验证方法自洽)。读出的算法:

    | x | <= pi/4 : 直接双精度多项式
    |x| < 2^-13 : cosf -> 1.0f ;sinf -> x(恒等)
    |x| < 2^-7 : cosf -> (float)fma(-(x*0.5), x, 1.0) ;
    sinf -> (float)fma(-(x*x*x), 1/6, x)
    否则 : z=x*x;cos 多项式 p = A3 + z(A5 + z(A7 + z*A9)),结果 fma(p, z*z, 1 – 0.5*z)
    sin 多项式 p = B3 + z(B5 + z(B7 + z*B9)),结果 fma(p, x*z, x)
    | x | > pi/4 : Cody-Waite 归约
    n = (int)trunc( fma(|x|, 2/pi, 0.5) ) ; r = fma(-n, PIO2_HI, |x|) – n*PIO2_LO
    cosf: n 奇 -> sin 多项式(参数 r),偶 -> cos 多项式;符号位 ^= ((n&3)+1)>>1
    sinf: n 奇 -> cos 多项式(参数 r),偶 -> sin 多项式;符号位 ^= (n&3>=2) ^ sign(x)
    最终一次 (float) 舍入

    常量(.rdata 按位型读出):2/pi = 0x3fe45f306dc9c883、PIO2_HI = 0x3ff921fb54400000、PIO2_LO = 0x3dd0b4611a626331、A3..A9 = 1/24, -1/720, 1/40320, -1/3628800、B3..B9 = -1/6, 1/120, -1/5040, 1/362880。

    复刻要点:多项式系数必须按「213 形式」写——vfmadd213sd xmm1, xmm3, mem 是 xmm1 = xmm3*xmm1 + mem,不是 xmm3*mem + xmm1;写反会让整个多项式量级错(本次第一版就把 cosf 做成 98% 不一致,定位到这条即修好)。|x| ≥ 0x4f490fdb(cosf)/ 0x4b800456(sinf)走 Payne-Hanek 慢路,RoPE 不可达(angle ≤ max_seq),退化为 device 库函数。

    仓库里落地后的 device 复刻(src/npu/cuda/vllm_cuda_kernels.cu,sinf_ucrt 同法):

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu,cosf_ucrt() */
    /* host 侧对照真实 UCRT 对拍 600002 组样本 → cosf/sinf 各 0 差异。 */
    static __device__ __forceinline__ double vc_ucd(unsigned long long b) {
    return __longlong_as_double((long long)b);
    }
    __device__ __forceinline__ float cosf_ucrt(float xf) {
    const double INV_PIO2 = vc_ucd(0x3fe45f306dc9c883ull);
    const double PIO2_HI = vc_ucd(0x3ff921fb54400000ull);
    const double PIO2_LO = vc_ucd(0x3dd0b4611a626331ull);
    const double A3 = vc_ucd(0x3fa5555555555555ull); /* 1/24 */
    const double A5 = vc_ucd(0xbf56c16c16c16c16ull);
    const double A7 = vc_ucd(0x3efa01a01a01a019ull);
    const double A9 = vc_ucd(0xbe927e4fb7789f5cull);
    ...
    const unsigned au = __float_as_uint(xf) & 0x7fffffffu;
    if (au >= 0x7f800000u) return cosf(xf);
    const double xd = (double)xf;
    if (au <= 0x3f490fdbu) { /* |x| <= pi/4 */
    if (au < 0x39000000u) return 1.0f; /* |x| < 2^-13 */
    ...
    const double z = xd * xd;
    double p = __fma_rn(z, A9, A7);
    p = __fma_rn(z, p, A5);
    p = __fma_rn(z, p, A3);
    return (float)__fma_rn(p, z * z, 1.0 – 0.5 * z);
    }
    ...
    }

    验证(实测):host 复刻对 UCRT 对拍 600002 组样本([-10000,10000] 均匀 + [-35,35] 密集 + 随机)→ cosf/sinf 各 0 差异;随后移植为 device cosf_ucrt/sinf_ucrt。RoPE 内核里按档位选择(src/npu/cuda/vllm_cuda_kernels.cu):

    /* 文件名:src/npu/cuda/vllm_cuda_kernels.cu,k_rope(…, bitexact) */
    const float ca = bitexact ? cosf_ucrt(angle) : cosf(angle);
    const float sa = bitexact ? sinf_ucrt(angle) : sinf(angle);

    四、实测数据

    实测(–stream-n 4,48 层 → 每 token 530 key,4 token 共 2120):

    对比不一致 key首个差异
    纯 CPU vs VLLM_CUDA_ATTN=1 VLLM_CUDA_ATTN_BITEXACT=1(CPU + 位级 attention) 0 / 1936 无
    纯 CPU vs 全 GPU decode,未加 FORCE_CPU 1917 / 1936 t32_l0_ks(伪差异,见 3.5)
    纯 CPU vs 全 GPU decode(DECODE+INFER+FORCE_CPU+L2EXACT) 2054 / 2120 t32_l0_x(MoE FFN 输出)

    层 0 逐字段(数据流序):

    ok _nrm ok _kraw ok _knorm ok _k ok _ks ok _v ok _vs
    ok _qrope ok _attn ok _ores
    DIFF _x <– 唯一残项:MoE FFN 输出

    结论:全 GPU decode 在 opt-in 档(VLLM_CUDA_L2EXACT=1)下,层 0 的整条注意力路径(RMSNorm → QKV GEMM → QK-norm → RoPE → KV 量化 → attention → O 投影+残差)已与纯 CPU 逐字节一致;沿数据流继续推进,第一个差异点就是 MoE FFN。

    五、边界与已知限制

    已完成(位级,opt-in)

    项开关位置
    UCRT expf 位级复刻 VLLM_CUDA_ATTN_BITEXACT / L2EXACT expf_ucrt(.cu)
    UCRT cosf/sinf 位级复刻 VLLM_CUDA_L2EXACT cosf_ucrt/sinf_ucrt(.cu)
    attn RMSNorm / QK-norm 同序 VLLM_CUDA_L2EXACT k_rms_norm_cpu/k_qk_norm_cpu
    QKV/O/GU/DOWN/LMHEAD GEMM 块内归约序 + nsplit=1 VLLM_CUDA_L2EXACT k_gw_gemm_f32act(pairwise)
    K 段投影 / Q 段 RoPE 对齐 VLLM_CUDA_L2EXACT k_rope(…, bitexact)
    decode attention 镜像 VLLM_CUDA_L2EXACT(或 ATTN_BITEXACT) k_attn_decode_online
    L2 harness(530 key/token) VLLM_L2_DUMP 两侧 k_l2_hash/st_l2_dump

    未完成 / 已知墙

    项现状结论
    MoE FFN(router / gate_up / down / topk) _x 起分歧 本轮唯一残项。三维天然一致(激活 s16 / 32-block scale、块内 int32 精确点积、权重 4×4 72B tile 布局);四处结构性不同:①router GEMM 是 64 路树归约 vs CPU 2048 步串行链(作者注释自认 relaxed bit-exactness);②scale 结合律 (dot*s_w)*s_a vs (s_w*s_a)*dot;③down 把 p 折叠进块项 vs 末尾乘一次;④silu 用 device expf 而非现成 expf_ucrt。要对齐需改写 4 个内核并放弃其现有并行度,故仅作 opt-in 档候选,本轮未做(已在 第 22 篇 收口)。
    全 GPU decode 的逐层位级全等(L2 整体) 未达成 被 MoE 阻断;层 0 注意力路径已通
    prefill/batch 路径位级 未做 本轮只覆盖 M=1 decode
    缺 FORCE_CPU 的生产配置 VLLM_CUDA_DECODE=1 VLLM_CUDA_INFER=1 仍是 greedy 级;位级档须显式开 L2EXACT(+FORCE_CPU 保 prefill 干净)

    CPU 对照(迁移前基线)

    • CPU 参考:kestrel-llm/src/model/vllm_safetensors.c:f32x8_hsum / hsum_neon4 —— CPU 归约序(8 元素树 / 4 元素树);超越函数直接用 UCRT 库 expf/cosf/sinf。
    • 迁移要点:跨硬件位级的拦路虎是超越函数——反汇编 ucrtbase.dll 位级复刻 expf_ucrt / cosf_ucrt / sinf_ucrt;RMSNorm/QK-norm 改串行同序(k_rms_norm_cpu / k_qk_norm_cpu);QKV GEMM 加 pairwise+nsplit=1;attention 改 online softmax 镜像。
    • 真机验证:未独立复现:需 30B + VLLM_L2_DUMP(530 key/token,2120 key 一次跑完)与位级档 A/B。

    六、小结

  • 对拍参考要锁死 token 位置;top-1 对拍会漏 ULP。 跨位置比较会把一个 token 的翻转误判成另一个 token 的翻转;要证位级,必须下沉到完整 logits 逐字节(或逐层 hidden/KV 哈希)。
  • CPU↔GPU 与 batch↔M=1 是两个正交口径。 前者可经「函数级对齐」做到位级,后者是投机红线。
  • 跨硬件位级的真正拦路虎是 transcendental 函数。 算子顺序对齐不够——expf/logf/rsqrt/cosf/sinf 的实现在不同运行库间不同。本次若止步于「换 (float)exp(double)」就只能到 56/64;只有把 UCRT 的 expf 位级复刻(含其自身 0.007% 的非正确舍入行为)才到 64/64。同理 1.0f/sqrtf 与 UCRT 逐位一致(rsqrtf 差 34.9%)、device cosf/sinf 差 ~20%。逆向运行库函数是可行路径:dumpbin /exports 定 RVA → objdump -d 读算法 → 运行时读常量表 → 复刻 → 用大样本探针证 0 差异;比「盲拟合多项式」可靠得多。写 FMA 时要分清 132/213/231 形式——把 vfmadd213sd 写成 xmm3*mem + xmm1 会让多项式整体错位(本次 cosf 首版 98% 不一致就是这个)。
  • L2 harness 要按 key 哈希、可二分。 纯 token 序列只能判「是否」,逐层+KV+scale 的哈希 key 才能把首个差异钉到具体层与具体量——本次一路钉到 t32_l0_kraw → _qrope → _attn → _x,没有一步靠猜。
  • 对拍配置本身要干净;构建脚本的管道会骗人。 给被测路径加「预载权重」这类开关时,很容易连带改动参考侧的上游(本次 VLLM_CUDA_INFER=1 让 prefill 也上了 GPU,使 KV 行整体偏离,伪装成 layer 0 的 scale 问题)。对拍前先确认「参考侧与被测侧的唯一差别就是被测的那一段」。另外,把 powershell -File build.ps1 | Select-String … 和多条后续语句写在同一段命令里时,出现过 exe 没被重链、却仍显示 rc=0 的情况,导致拿着旧二进制读结论——构建必须重定向到日志文件并单独校验产物时间戳。

  • 相关篇目:第 16 篇、第 17 篇、第 22 篇 源码与配套资源:本仓库 https://gitee.com/pei-xiaoguang/kestrel-llm-cuda.git; CPU 推理源码 https://gitee.com/pei-xiaoguang/kestrel-llm

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Kestrel_VLLM - 18 · 投机解码位级对拍收口:batch 与 M=1 一致,注意力路径与 CPU 逐字节对齐
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!