欢迎光临
我们一直在努力

07 · 硬件后端·下

系列:gdev-master(NVIDIA/nouveau 用户态 GPGPU 运行时)从 C/C++ 到 Rust 的移植工程 已发布:01 调研与规划 · 02 工程化工作流 · 03 实施进度与踩坑 · 04 解析器 · 05 数据流分析 · 06 硬件后端·上 本篇: 07 硬件后端·下


这篇收尾 P4,也收尾整套移植到目前为止的全部四个阶段。后半段两件事:nve4 Kepler SASS(照译保留的第二架构)和 pscnv 裸 ioctl 双后端(sdaa 原型,覆盖 driver=pscnv 配置)。

一、nve4:256 字节位域结构体的 LSB-first 拍平

nve4 最大的难点是 gdev_nve4_compute_desc——一个 256 字节的位域结构体,用来描述 Kepler 的 compute 命令。C 里的位域布局是「LSB-first」:靠编译器把一堆 unsigned x : n 的位段拍平进字节,位段之间还有 padding、有跨越字节边界的拆包。

Rust 没有位域,这条路只能手拍:把 256 字节当一个 #[repr(C)] 结构体,每个字段用 u32 整字承载,靠位运算读写位段。危险在于手算位偏移必错——所以照 P4 的老规矩,用 gcc 实测:拿 C 结构体喂各种字段组合,把真实布局的 golden 复现出来。

#[repr(C)]
struct GdevNve4Cb {
address_l: u32, // 0x00
word: u32, // 0x04 bit0-7=address_h, bit15-31=size
}

#[repr(C)]
struct GdevNve4ComputeDesc {
unk0: [u32; 8], // 0x00
entry: u32, // 0x20
unk9: [u32; 3], // 0x24
griddim_x: u32, // 0x30
griddim_y: u16, // 0x34
griddim_z: u16, // 0x36
// …
shared_size: u16, // 0x44
blockdim_x: u16, // 0x4a
cb_mask: u32, // 0x50 bit0-7=cb_mask, bit29-30=cache_split
cb: [GdevNve4Cb; 8], // 0x74
local_size_p: u32, // 0xb4 bit0-19=local_size_p, bit27-31=bar_alloc
local_size_n: u32, // 0xb8 bit0-19=local_size_n, bit24-31=gpr_alloc
cstack_size: u32, // 0xbc bit0-19=cstack_size, bit20-31=unk47_20
unk48: [u32; 16], // 0xc0
} // 总 0x100 = 256 字节

编译期断言钉死布局(size == 0x100、entry == 0x20、griddim_y == 0x34、shared_size == 0x44、cb == 0x74、cb[1] == 0x7c、unk48 == 0xc0)。位域的写入表达式全用 gcc 实测的 golden 复现,六组:

赋值所在 word offsetgcc 实测 word 值
griddim_x = 0x1234567 0x30 0x01234567
cache_split=2, cb_mask=0x5 0x50 0x40000005
cb[0].size=0x12345, address_h=0xAB 0x78 0x91A280AB
local_size_p=0x54321, bar_alloc=0x1F 0xb4 0xF8054321
local_size_n=0x11111, gpr_alloc=0xFF 0xb8 0xFF011111
cstack_size=0x22222, unk47_20=0x300 0xbc 0x30022222

对应的写入表达式,比如 cb 的 word 字段是 (size << 15) | (address_h & 0xFF),local_size_p 是 (v & 0xFFFFF) | (bar_alloc << 27)。单测就是拿这些 golden 值反向钉死位运算。

和位域纠缠在一起的是 ring 的 4 个变体——__gdev_begin_ring_nve4(0x2)/_const(0x6)/_il(0x8)/_1l(0xa)。前两个和 nvc0 编码逐字节相同但 C 里是独立函数名,为忠实独立定义;_il/_1l 是新编码(0x8001_206C / 0xA001_20C0)。

二、7 条 C bug 忠实 + 两个位域/常量陷阱

nve4 这一刀最见「C 是 oracle」功夫的,是 7 条 C bug 全部忠实照抄,逐条:

  • PCOPY0 → PCOPY1 subchannel:nve4_fence_write 的 PCOPY0 case 里却用 GDEV_SUBCH_NV_PCOPY1 编码(C:384-389)。照抄不修。
  • (int)desc.addr >> 8 算术右移:地址是 u32,as i32 后算术右移 8 位——高位为 1 时得负数。Rust 必须 ((*ctx).desc.addr as i32 >> 8) as u32,写成逻辑右移就错。
  • p2mf 是空实现:C 里 #if 1 分支实际只 GDEV_PRINT("not implemented") + fire_ring,不拷数据。照译,不是漏。
  • init 的 gdev_query 被原版注释掉了:init 不查 MP_COUNT,直接跳过硬件限制设置。照译「不启用」。
  • entry = code_pc:注释写 code_addr>>8 但实际赋 code_pc,指错字段。照赋值,不照注释。
  • 潜在除零不防御:mp_count = lmem_size_total/48/warp_lmem_size 若 warp_lmem_size=0 则 C UB,实际不触发,忠实直译。
  • debug_print 残缺跳过:__nve4_launch_debug_print 引用未定义的 desc 且多一个 }——GDEV_DEBUG 未定义整段跳过,不影响。
  • 另外两个「看起来像笔误但其实是 C 原值」的细节,独立验收时逐函数核对 .c 才坐实:

    • cb_mask = 0 只清低 8 位,不清 cache_split:位域赋值的 C 语义陷阱——cb_mask 是 bit0-7,cache_split 是 bit29-30,赋 0 只清低 8 位。fill 里要 cb_mask = (cache_split&0x3)<<29 后 &= !0xFF 再循环 |= 1<<x,顺序照 C。
    • FLUSH 常量是 0x1000 而非 0x1001:这种最容易在「看着不对」时手滑修掉的地方,恰恰是 C 原值,核对 .c 后钉死。

    完成后,P4 的前向未定义符号清零:之前 P4-03 留的 nve4_compute_setup/nvc0_compute_setup extern 占位全部落地。

    三、pscnv:cargo feature 切出第二个后端

    P4-08 收尾的是 pscnv 裸 DRM ioctl 后端——它和 nouveau 提供完全相同的 21 个 gdev_raw_* 接口,区别在于它直接 ioctl() 发 PSCNV/DRM 命令,不经过 libdrm。它其实是 sdaa 分支里 aidev 的原型(diff 已证同源),目标机用 nouveau、不跑它,但为了覆盖 driver=pscnv 这档配置,一并译。

    这一刀的量比想象大,共 54 个对象:21 个 gdev_raw_*(ioctl 版)+ 23 个 pscnv_* ioctl 包装 + 5 个 pscnv_ib_* + drmIoctl/drmCommandWrite/drmCommandWriteRead + _ioc/drm_ioc 两个 const fn;还有 20 个 repr© 结构体,gcc 实测断言——其中 phys_getaddr 是 32 字节,纠了 spec 初判的 24。

    ⛔ 更正(2026-09-18 复量):本段原写「55 个对象 / 19 个 repr© 结构体」,两处都错。 ① 五个分项实测是 21 / 23 / 5 / 3 / 2(pscnv_* 共 28 个,其中 5 个是 pscnv_ib_*,28−5=23),和 = 54,与 55 差 1;无任何读法能得 55(tasks/P4-08.md:37 的「21 导出 + 1 static inline」是 22,那是文件内对象数,不是导出数)。命令:cd rust/gdev-rs/gdev/src/p4/pscnv && grep -rh 'pub unsafe extern "C" fn gdev_raw_' *.rs | wc -l(=21)、… 'fn pscnv_' | wc -l(=28)、… 'fn pscnv_ib_' | wc -l(=5)、… 'fn drm' | wc -l(=3)、grep -rh 'pub const fn ' *.rs | wc -l(=2)。 ② 结构体实测 20(15 个 drm_pscnv_* + 3 个 drm_gem_* + 2 个 pscnv_ib_*),布局断言也是 20 条。命令:grep -rh -A1 '^#\\[repr(C)\\]' *.rs | grep -c 'pub struct'。19 是 2026-09-18 内部更正前的旧数(tasks/P4-08.md:186 已改「15+3+2」、STATUS.md 已改 20),本篇当时没跟上。

    难点不在翻译,在两个后端的符号互斥:nouveau 和 pscnv 都想导出 gdev_raw_*,不能同时编进一个 .so。解法是落地一套架构:

    • cargo feature pscnv + 一个 backend.rs 再导出层——15 个非 mem 的 gdev_raw_* 在 backend 里按 feature 双分支再导出;
    • 3 个文件里的 use …nouveau… 统一改成 use …backend…;
    • build.rs 按 CARGO_FEATURE_PSCNV 决定要不要链接 libdrm(pscnv 裸 ioctl 不链)。

    ioctl 命令号是这套东西的命门,_ioc 用 const fn 拼 (dir<<30)|(size<<16)|(type<<8)|(nr),从系统 asm-generic/ioctl.h 逐位核对(非推断):

    const fn _ioc(dir: u32, ty: u32, nr: u32, size: u32) -> u32 {
    (dir << 30) | (size << 16) | (ty << 8) | nr
    }
    const fn drm_ioc(dir: u32, nr: u32, size: u32) -> u32 {
    _ioc(dir, DRM_IOCTL_BASE, DRM_COMMAND_BASE + nr, size) // ty='d'=0x64
    }

    五组 golden 全复现:DRM_IOCTL_GEM_CLOSE=0x40086409、GEM_FLINK=0xC008640A、GEM_OPEN=0xC010640B、GEM_NEW=0xC0406460、GETPARAM=0xC0106440。

    忠实点也照抄:pscnv_ib_push 的 len/flags 同位 <<40 quirk(flags 会覆盖 len,但调用点 flags 恒 0)、pscnv_vm_read 的 buf_wr 未初始化 → 零定义化、ctx_new 只设 push/update_get(space/kick 保持 NULL,与 nvidia_ring 判空兼容)。还有 pscnv_ib_chan_new/bo_alloc 恒返 1 的 goto 清理链(失败返 1 非 errno)。

    四、双轨对照:P4 最硬的一次验证

    这套东西的验证是 P4 最硬的一次「双轨对照」:

    • 默认构建:nm -D 311 个符号与 P4-07 基线逐字一致(diff 为空,零泄漏——backend 再导出层没让任何符号丢失或错位);
    • –features pscnv:nm -D 是 21 gdev_raw_* + 28 pscnv_* + 3 drm*,零 nouveau 泄漏;readelf -d 实测 DT_NEEDED 没有 libdrm——真·裸 ioctl,没偷偷链上 libdrm。

    五、P4 收口:做完 ≠ 验完

    P4 八任务到这里全部收口。但和 06 篇强调的一样,这里要再钉一句实话:「全绿」覆盖的是编译 + 布局 + 符号 + 纯逻辑单测四层;命令流字节级等价(层 3)和真硬件 run 都还没做。

    当时最大的缺口,恰恰是价值最高的那一块——P4-03/05/06/07 的端到端验收。其中层 3(mock-fifo harness 逐字节比对)紧接着就补上了,见 08 篇;剩下的真硬件 run 只能等目标机 GTX 580。不假装「全绿 = 做完」。

    六、这一步的账(P4 收口时)

    • 测试:从 0 滚到 805 全过(–features pscnv 808)——这是 P4 收口时的数;加上随后的层 3 oracle 与 P3 收尾,915 / 918,全景账见运行时API.md。
    • 符号:nm -D 311 个导出符号与 C 基线逐字一致(P4 收口时的数;P3-09 落地 103 个 CUDA Runtime 符号后为 414),双后端零泄漏。★ 口径注:311 / 414 / 422 是同一把尺子(T/W/D/B/V 去掉 _Z* 与链接器符号的全量集合差)在三个时点的读数 —— P4 收口 311 → P3-09 落 103 个 CUDA Runtime 符号 → 414 → 补-02e 补 8 个真符号 → 422。414→422 是增长(gdev_vsched 族 5 + gdev_cuda_load_cubin_ptx + __gdev_init_device + min2),不是换尺子。

      ⛔ 本节曾写过一条相反的口径注(「311/414 是旧口径(只量两个名字族)的读数,414→422 是换了尺子」),那条是错的,独立复量后更正。错在把两件事混了:「只量两个名字族」是层 5 旧版的筛选口径(^cu[A-Z]|^cuda[A-Z] + gdev_raw_,对族外缺口完全沉默,门-11,见 12 篇),它与 311/414 无关 —— 该口径实测只有 249 个(命令:nm -D –defined-only rust/gdev-rs/target/debug/libgdev_rs.so | awk '{print $3}' | grep -E '^(cu[A-Z]|cuda[A-Z]|gdev_raw_)' | sort -u | wc -l)。而那次改口径的提交信息自己写的是「全量集合差(C 两库 422 参照 vs Rust 414)」——414 是新口径下的读数。⇒ 教训:更正本身也是断言,也要复量,不能凭「commit 信息里提到了两个名字族」就推断某个数是那个口径的计数。

    • 阶段:P0 类型常量 → P1 核心 → P2 Driver API → P3 Ocelot(ir/parser/analysis)→ P4 硬件后端(nouveau + nvc0/nve4 + pscnv)。P3 剩余三块(transforms / executive / Runtime API)按价值排序放到 P4 之后。
    • 未验(诚实清单):真硬件端到端行为,等目标机(Fedora 18 / GTX 580 / glibc 2.16)部署后实跑确认。层 3 命令流字节等价随后补齐。

    七、下一步

    P4 硬件后端到此收口。下一步先把这层的层 3 oracle 补上—— mock fifo + C harness 逐字节比对;再往后才是真机验收(目标机部署回归 + GTX 580 实跑 test/gdev / test/cuda)。

    赞(0)
    未经允许不得转载:171主机测评 » 07 · 硬件后端·下
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址