Module 10:Phase 1 — 硬件抽象层(HAL)
把模型代码从 D3D12 细节中解耦出来:Device / Buffer trait、状态跟踪 buffer、Executor、通用 operator wrapper、compute kernel 路径与 GPU timer。
学习目标
- 理解 HAL 的核心目标:让模型层依赖“能力”,而不是依赖具体 GPU API。
- 能设计最小的
Device/Buffertrait,并说明每个方法的意义。 - 理解 D3D12 resource state tracking 与 transition barrier 的必要性。
- 理解 one-shot command executor 如何封装 allocator、command list 和 fence。
- 区分 DirectML operator wrapper 与 custom compute kernel path。
- 使用 GPU timestamp profiling 测量 kernel 时间。
10.1为什么需要 HAL?
如果 xinfer-model 直接操作 ID3D12Resource、root signature 和 descriptor heap,模型代码会被 Direct3D 12 的对象生命周期、资源状态和绑定规则占据。后续接入 Xbox host-created device、尝试其他 backend,或在测试中使用 CPU reference,都会被平台细节牵连。
HAL(Hardware Abstraction Layer)的作用是给模型层提供较小且稳定的能力接口。模型层依赖 Device / Buffer 等 trait;D3D12/DirectML、未来的其他 backend 或 CPU fallback 负责实现这些 trait。
10.2Device / Buffer trait 的最小设计
Phase 1 中,xinfer-backend 的最小接口保持克制:Buffer 只暴露字节数;Device 负责分配、上传和下载 buffer。这个接口已经足以支撑权重加载、activation buffer、中间结果和 readback,同时不泄露 D3D12 类型。
pub trait Buffer {
fn size_in_bytes(&self) -> usize;
}
pub trait Device {
type Buffer: Buffer;
fn alloc(&self, shape: &Shape, dtype: DType) -> Result<Self::Buffer>;
fn upload(&self, data: &[u8], shape: &Shape, dtype: DType) -> Result<Self::Buffer>;
fn download(&self, buffer: &Self::Buffer) -> Result<Vec<u8>>;
}
一个重要细节是:HAL 不直接暴露 D3D12 类型。模型层只需要知道“这是一个 buffer”,而不需要知道它背后是 ID3D12Resource、Vulkan buffer 还是 CPU vector。
HAL 的接口不应过早抽象所有可能 backend 的能力。先抽象当前模型真正需要的最小能力;当新 backend 或新需求出现时再扩展。
10.3状态跟踪 GPU buffer:自动 transition barrier
D3D12 的资源有显式状态:同一个 buffer 在不同时间可能是 COPY_DEST、COPY_SOURCE、 NON_PIXEL_SHADER_RESOURCE 或 UNORDERED_ACCESS。如果状态不对,GPU 行为未定义,调试层会报错。
因此 DmlBuffer 除了保存 ID3D12Resource,还保存当前状态:
pub struct DmlBuffer {
pub resource: ID3D12Resource,
pub size: u64,
pub shape: Shape,
pub dtype: DType,
state: Cell<D3D12_RESOURCE_STATES>,
}
当 custom kernel 需要把 buffer 作为 SRV 读取时,ComputeKernel::dispatch_grid 会调用 ensure_state(..., NON_PIXEL_SHADER_RESOURCE);作为 UAV 写入时调用 ensure_state(..., UNORDERED_ACCESS)。download_buffer 会把源 buffer 转到 COPY_SOURCE,upload_buffer 则先在 default heap 中创建 COPY_DEST,拷贝后转为 UNORDERED_ACCESS。如果当前状态已经匹配,ensure_state 不记录 barrier;否则写入 transition barrier 并更新状态。
10.4One-shot Executor:封装 command list + fence
D3D12 提交命令需要 command allocator、command list、command queue、fence 和 event 协同工作。若每个调用点都直接操作这些对象,代码会重复,并且容易遗漏 close、signal、wait 或 reset。
crates\xinfer-dml\src\resource.rs 将它们封装为 Executor:
let mut exec = dev.executor()?;
kernel.dispatch(&mut exec, ...)?;
exec.submit_and_wait()?;
exec.reset()?;
Executor::submit_and_wait 关闭 command list、提交到 queue、递增 fence 并阻塞等待完成;reset 复用 allocator 和 list;uav_barrier 用于在同一 command list 中排序相互依赖的 UAV 写读。后续把一个 decoder layer 的多个 dispatch 合并提交时,这个封装是关键基础。
10.5通用 DirectML operator wrapper 与 compute-kernel path
Phase 1 中保留两条执行路径,分别服务于 DirectML 已提供的 operator 和项目自写 kernel:
DmlOperator:封装 DirectML operator 的 create / compile / initialize / bind / dispatch。ComputeKernel:封装自写 HLSL compute shader 的 root signature / PSO / root descriptors / dispatch。
二者都向 D3D12 command list 记录工作,但绑定方式不同。DmlOperator 需要 DirectML binding table、temporary resource 与 persistent resource;ComputeKernel 使用 root constants 以及 SRV/UAV root descriptors,适合 RMSNorm、RoPE、SwiGLU、attention 等自写 HLSL kernel。
| 路径 | 适合 | 主要封装 |
|---|---|---|
DmlOperator | DirectML 提供的通用 operator | binding table、temp/persistent resource |
ComputeKernel | 自写 HLSL kernel | root constants、root SRV/UAV descriptors、PSO |
10.6GPU timestamp profiling
CPU wall-clock 包含命令提交、驱动调度、queue 排队和等待开销。若要测量一段 GPU 工作本身的耗时,需要在 command stream 中写 timestamp query。GpuTimer 的实现包含两个 query、一段 readback buffer,并使用 command queue 的 timestamp frequency 换算毫秒。D3D12 timestamp 的基本流程是:
- 创建 timestamp query heap;
- 在 command list 中写 begin timestamp;
- 记录被测 dispatch;
- 写 end timestamp;
- resolve query data 到 readback buffer;
- 用 queue timestamp frequency 换算毫秒。
性能优化应以测量结果为依据。xinfer 后续优化显示,早期主要瓶颈并非单个 matmul,而是过多 GPU submission 和 logits readback。GPU timestamp 能把这类问题从推测转化为可比较的数据。
Lab 10实现 upload / download + GPU timer
本实验对应 Phase 1。目标是通过 HAL 完成 buffer round-trip,并用 GPU timestamp 测量一段已记录的 GPU 工作。
- 实现
DmlDevice::alloc_buffer。 - 实现
DmlDevice::upload_buffer:用 upload heap → default heap。 - 实现
DmlDevice::download_buffer:default heap → readback heap。 - 实现
Executor:record、submit、wait、reset。 - 实现
GpuTimer,给一个 GEMM 或 affine kernel 计时。
# 验收测试
cargo test -p xinfer-dml --test hal
# 期望包含:
buffer_roundtrip ... ok
gpu_timer_reports_nonnegative ... ok
小结
Module 10 将 Phase 0 的 GPU 闭环提升为 HAL:xinfer-backend 定义 Device / Buffer trait,xinfer-dml 以 DmlBuffer 跟踪 D3D12 resource state,以 Executor 统一 command list 与 fence,以 DmlOperator 和 ComputeKernel 分别封装 DirectML operator 与 HLSL kernel。GpuTimer 提供 GPU 侧时间戳,为后续定位 submission、readback 和 kernel 耗时奠定基础。
思考与练习
基础为什么模型层不应该直接依赖 ID3D12Resource?
因为那会把模型逻辑和 D3D12/Windows 绑死,破坏可移植性与可测试性。模型层应只依赖抽象的 Buffer trait;这样换 backend(Vulkan/Metal)或在 CPU 上做参考测试时无需改模型代码,HAL 边界清晰。
基础upload heap、default heap、readback heap 分别用于什么?
upload heap:CPU 可写、GPU 可读,用于 把数据从 CPU 传到 GPU(权重/输入上传的中转)。default heap:GPU 本地显存,访问最快,计算用的 buffer 常驻于此(CPU 不可直接访问)。readback heap:GPU 可写、CPU 可读,用于把结果从 GPU 拷回 CPU。典型流:upload→default(计算)→readback。
进阶解释 resource state tracking 如何减少人为 barrier 错误。
每个 buffer 携带其当前状态(如 UAV / COPY_DEST / COPY_SOURCE / COMMON)。当要做某操作时,HAL 比较“当前状态”与“所需状态”,仅在不一致时自动插入正确的 transition barrier 并更新记录。这样开发者不必手算每处该加什么 barrier,避免漏加(数据竞争)或加错(状态不匹配导致 device removed),把易错的状态机交给统一封装。xinfer 的 DmlBuffer 即采用状态跟踪。
进阶为什么 GPU timestamp 比 CPU wall-clock 更适合测单个 kernel?
CPU wall-clock 测的是“提交到等待返回”的端到端时间,包含命令提交、驱动开销、CPU/GPU 不同步、队列排队等噪声,且 GPU 异步执行使 CPU 计时点对不准。GPU timestamp query 直接在命令流中、kernel 前后打 GPU 时间戳,按 GPU 时钟测真正的执行耗时,排除 CPU 侧噪声,分辨率更高,适合逐 kernel 剖析。xinfer 的 GpuTimer 即基于 timestamp query。
挑战扩展 Device trait,使其能创建一个“未初始化但指定用途”的 buffer,并说明对 D3D12 状态的影响。
新增方法如 fn alloc_uninit(&self, bytes: usize, usage: BufferUsage) -> Buffer,其中 usage 是抽象枚举(如 Storage/CopyDst/CopySrc)。它分配 default heap 资源但不上传数据。
D3D12 影响:创建时需给一个初始 resource state(如 COMMON 或按 usage 选 UNORDERED_ACCESS/COPY_DEST),并在 Buffer 的状态字段里记录。由于内容未初始化,首次使用前要么先写(作为 UAV/COPY_DEST),要么明确语义上的“先写后读”;状态跟踪据此在首次操作时插入合适 barrier。usage 提示让 HAL 选更优初始状态,减少首个 barrier。这类 buffer 适合做计算中间结果或 KV cache 等“先分配后填充”的场景。