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=
1773vs 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 差的位置。
ini
[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-1 | token |
|---|---|---|
| 两遍 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 路径),读出算法与常量:
ini
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 表略):
c
/* 文件名: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):
c
/* 文件名: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);
}
c
/* 文件名: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。位级对拍的正确命令:
ini
# 参考:纯 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):
c
/* 文件名: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 一致,验证方法自洽)。读出的算法:
scss
| 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 同法):
c
/* 文件名: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):
c
/* 文件名: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 逐字段(数据流序):
lua
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%)、devicecosf/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 篇 源码与配套资源:本仓库 gitee.com/pei-xiaogua...; CPU 推理源码 gitee.com/pei-xiaogua...