同一个 NMS 算子,两种语言实现:CUDA C 与 Rust 生成 LLVM IR 和 Vortex 汇编的差异
0. 摘要
- 同一个两段式 NMS(build_masks + resolve_keep),CUDA C(
__global__+ chevron)
与 Rust(#[no_mangle] extern "C"+kernel!(<<<g,b>>>()proc-macro)各写一版,
编译产物都是单个 host ELF 内嵌 kernel 镜像 ,运行结果一致:keep (3): 5 0 3。 - LLVM IR 层面差异明显 :前端不同,IR "味道"完全不同------
CSR 读取是内联asm "csrr ..."+llvm.riscv.vx.uniform固有函数©还是
tail call @vxr_thread_id_x()外部调用(Rust);
行清零是显式 store 循环还是llvm.memset.p0.i64;
比较是icmp slt还是icmp ult;
符号是 Itanium mangled 还是no_mangle;
最本质的一条:C 侧 IR 带!vortex.kargs参数 ABI 元数据,Rust 侧没有------
这直接决定了机器码层参数解包方式(见 §5.1)。 - Vortex 机器码层面差异很小 :IoU 热点循环几乎逐指令一致
(同样的fmax.s/fmin.s/fsub.s/fmul.s/fadd.s/flt.s/fdiv.s+ 同样的
vx_split/vx_join/vx_pred_n分歧控制流指令),只有寄存器分配、
kentry 参数偏移(8B 槽位 vs 原生对齐)、CSR 访问方式(内联csrrvs
jal到 3 条 stub)和 Rust 镜像里多拖进来一个memset的差别。 - 结论:语言差异在前端(源码→IR)就被"翻译"成了两种不同的 IR 形状,
但 Vortex 后端(机器 pass:kentry 包装、参数解包、分歧控制流改写)
以 IR 元数据为契约 工作,因此最终机器码的热路径几乎相同------
后端才是唯一的事实来源,语言只是入口。
1. 算法与测试数据
NMS(non-maximum suppression)采用两段式 SIMT 实现:
build_masks(并行,每 lane 处理一个 box i):
输入 boxes 已按 score 降序上传。lane i 遍历所有更高分的框 j < i,
若IoU(i,j) > thr就在掩码第 i 行的第 j 位置 1
(masks[i*W + (j>>5)] |= 1u << (j&31),W = ceil(N/32))。resolve_keep(单 lane):
逐行扫描,keep[i] = 1当且仅当不存在 j < i 使keep[j]=1且
mask[i][j]=1(已被抑制的框不再抑制别人)。
IoU 用角点公式,fmaxf/fminf 写交叠宽高并 clamp 到 0。
测试数据(6 个框,x1,y1,x2,y2,score;thr = 0.5):
| 原始下标 | 坐标 | score |
|---|---|---|
| 0 | (100,100,210,210) | 0.90 |
| 1 | (105,105,215,215) | 0.85 |
| 2 | (102,102,212,212) | 0.80 |
| 3 | (300,300,400,400) | 0.70 |
| 4 | (302,302,402,402) | 0.65 |
| 5 | (500,500,600,600) | 0.95 |
按 score 排序后:5(0.95)、0(0.90)、1(0.85)、2(0.80)、3(0.70)、4(0.65)。
两簇互相重叠(簇内 IoU ≈ 0.72~0.83 > 0.5),簇间 IoU = 0 ⇒
期望 keep = {5, 0, 3} (原始下标,按 score 降序输出)。
两侧都内置与 nms.cu 同逻辑的 CPU 贪心参考实现做自检,
并做跨语言交叉核对(保留列表必须逐位相同)。
2. 代码
2.1 C 侧:tests/cuda_compat/nms.cu(原样使用,未改一行)
标准 CUDA C:__device__ __forceinline__ IoU、两个 __global__ kernel、
host 端 cudaMalloc/cudaMemcpy + chevron 启动 + CPU 参考。
cpp
// nms.cu --- compile: nvcc -O2 -arch=sm_70 nms.cu -o nms
#include <cuda_runtime.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <cmath>
#define CUDA_CHECK(call) \
do { \
cudaError_t _e = (call); \
if (_e != cudaSuccess) { \
fprintf(stderr, "CUDA error %s:%d: %s\n", __FILE__, __LINE__, \
cudaGetErrorString(_e)); \
std::exit(EXIT_FAILURE); \
} \
} while (0)
// ---------------------------------------------------------------------------
// 计算两个 box 的 IoU(角点格式 x1,y1,x2,y2)
// ---------------------------------------------------------------------------
__device__ __forceinline__ float dev_iou(
float ax1, float ay1, float ax2, float ay2,
float bx1, float by1, float bx2, float by2)
{
const float xx1 = fmaxf(ax1, bx1);
const float yy1 = fmaxf(ay1, by1);
const float xx2 = fminf(ax2, bx2);
const float yy2 = fminf(ay2, by2);
const float w = fmaxf(0.0f, xx2 - xx1);
const float h = fmaxf(0.0f, yy2 - yy1);
const float inter = w * h;
const float area_a = (ax2 - ax1) * (ay2 - ay1);
const float area_b = (bx2 - bx1) * (by2 - by1);
const float uni = area_a + area_b - inter;
return (uni > 0.0f) ? (inter / uni) : 0.0f;
}
// ---------------------------------------------------------------------------
// Kernel 1:构建抑制位掩码
// boxes : (N,4) 已按 score 降序排列
// masks : (N,W) W = ceil(N/32),第 j 位 = 1 表示框 j 抑制框 i
// ---------------------------------------------------------------------------
__global__ void build_masks_kernel(const float* __restrict__ boxes,
int N, int W,
float iou_thr,
unsigned int* __restrict__ masks)
{
const int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= N) return;
unsigned int* row = masks + (size_t)i * (size_t)W;
// 清零本行(避免依赖 cudaMemset)
for (int w = 0; w < W; ++w) row[w] = 0u;
const float ax1 = boxes[4*i+0], ay1 = boxes[4*i+1];
const float ax2 = boxes[4*i+2], ay2 = boxes[4*i+3];
// 只与更高分(索引更小)的框比较
for (int j = 0; j < i; ++j) {
const float bx1 = boxes[4*j+0], by1 = boxes[4*j+1];
const float bx2 = boxes[4*j+2], by2 = boxes[4*j+3];
const float v = dev_iou(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2);
if (v > iou_thr) {
row[j >> 5] |= (1u << (j & 31));
}
}
}
// ---------------------------------------------------------------------------
// Kernel 2:解析哪些框最终保留(单线程,N 一般不大)
// keep[i] = true 当且仅当 不存在 j<i 使得 keep[j]=1 且 mask[i][j]=1
// ---------------------------------------------------------------------------
__global__ void resolve_keep_kernel(const unsigned int* __restrict__ masks,
int N, int W,
unsigned char* __restrict__ keep)
{
if (blockIdx.x != 0 || threadIdx.x != 0) return;
for (int i = 0; i < N; ++i) {
const unsigned int* row = masks + (size_t)i * (size_t)W;
bool suppressed = false;
for (int j = 0; j < i; ++j) {
if (!keep[j]) continue; // 已被抑制的框不再抑制别人
if (row[j >> 5] & (1u << (j & 31))) {
suppressed = true;
break;
}
}
keep[i] = suppressed ? 0u : 1u;
}
}
// ---------------------------------------------------------------------------
// Host 端封装
// ---------------------------------------------------------------------------
struct BBox { float x1, y1, x2, y2, score; };
// 输入 boxes 无需有序,函数内部会按 score 降序排序。
// 返回保留的索引(相对于输入原始顺序)。
std::vector<int> nms_gpu(const std::vector<BBox>& boxes, float iou_thr)
{
const int N = (int)boxes.size();
std::vector<int> result;
if (N == 0) return result;
// 1) 按 score 降序排序
std::vector<int> idx(N);
for (int i = 0; i < N; ++i) idx[i] = i;
std::sort(idx.begin(), idx.end(),
[&](int a, int b){ return boxes[a].score > boxes[b].score; });
std::vector<float> h_flat(N * 4);
for (int i = 0; i < N; ++i) {
const BBox& b = boxes[idx[i]];
h_flat[4*i+0] = b.x1;
h_flat[4*i+1] = b.y1;
h_flat[4*i+2] = b.x2;
h_flat[4*i+3] = b.y2;
}
// 2) 分配显存
float* d_boxes = nullptr;
unsigned int* d_masks = nullptr;
unsigned char* d_keep = nullptr;
const int W = (N + 31) / 32;
CUDA_CHECK(cudaMalloc(&d_boxes, N * 4 * sizeof(float)));
CUDA_CHECK(cudaMalloc(&d_masks, (size_t)N * W * sizeof(unsigned int)));
CUDA_CHECK(cudaMalloc(&d_keep, N * sizeof(unsigned char)));
CUDA_CHECK(cudaMemcpy(d_boxes, h_flat.data(), N * 4 * sizeof(float),
cudaMemcpyHostToDevice));
// 3) 启动 kernel 1
const int threads = 256;
const int blocks = (N + threads - 1) / threads;
build_masks_kernel<<<blocks, threads>>>(d_boxes, N, W, iou_thr, d_masks);
CUDA_CHECK(cudaGetLastError());
// 4) 启动 kernel 2
resolve_keep_kernel<<<1, 1>>>(d_masks, N, W, d_keep);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());
// 5) 拷回
std::vector<unsigned char> h_keep(N);
CUDA_CHECK(cudaMemcpy(h_keep.data(), d_keep, N, cudaMemcpyDeviceToHost));
for (int i = 0; i < N; ++i)
if (h_keep[i]) result.push_back(idx[i]);
cudaFree(d_boxes);
cudaFree(d_masks);
cudaFree(d_keep);
return result;
}
// ---------------------------------------------------------------------------
// CPU 参考实现(用于对照验证)
// ---------------------------------------------------------------------------
static float host_iou(const BBox& a, const BBox& b) {
float xx1 = std::max(a.x1, b.x1);
float yy1 = std::max(a.y1, b.y1);
float xx2 = std::min(a.x2, b.x2);
float yy2 = std::min(a.y2, b.y2);
float w = std::max(0.0f, xx2 - xx1);
float h = std::max(0.0f, yy2 - yy1);
float inter = w * h;
float uni = (a.x2-a.x1)*(a.y2-a.y1) + (b.x2-b.x1)*(b.y2-b.y1) - inter;
return uni > 0 ? inter / uni : 0.0f;
}
std::vector<int> nms_cpu(const std::vector<BBox>& boxes, float thr) {
int N = boxes.size();
std::vector<int> order(N);
for (int i = 0; i < N; ++i) order[i] = i;
std::sort(order.begin(), order.end(),
[&](int a, int b){ return boxes[a].score > boxes[b].score; });
std::vector<char> removed(N, 0);
std::vector<int> keep;
for (int i = 0; i < N; ++i) {
int a = order[i];
if (removed[a]) continue;
keep.push_back(a);
for (int j = i + 1; j < N; ++j) {
int b = order[j];
if (!removed[b] && host_iou(boxes[a], boxes[b]) > thr)
removed[b] = 1;
}
}
return keep;
}
// ---------------------------------------------------------------------------
// 测试
// ---------------------------------------------------------------------------
int main() {
std::vector<BBox> boxes = {
{100, 100, 210, 210, 0.90f},
{105, 105, 215, 215, 0.85f},
{102, 102, 212, 212, 0.80f},
{300, 300, 400, 400, 0.70f},
{302, 302, 402, 402, 0.65f},
{500, 500, 600, 600, 0.95f},
};
float thr = 0.5f;
auto gpu_keep = nms_gpu(boxes, thr);
auto cpu_keep = nms_cpu(boxes, thr);
printf("IoU threshold = %.2f\n", thr);
printf("GPU keep (%zu):", gpu_keep.size());
for (int i : gpu_keep) printf(" %d", i);
printf("\nCPU keep (%zu):", cpu_keep.size());
for (int i : cpu_keep) printf(" %d", i);
printf("\n");
if (gpu_keep == cpu_keep) printf("✓ 结果一致\n");
else printf("✗ 结果不一致\n");
return 0;
}
注意 nms.cu 对 Vortex 工具链而言就是一份普通的 C++ 源:cuda_runtime.h
等由 sw/kernel/include/cuda.h 等兼容头接管,__global__ 展开为
annotate("vortex.kernel") used retain(见 §4.5 的 IR 注解),
chevron <<<g,b>>>(args) 被前端 Sema 改写成 host stub 调用。
2.2 Rust 侧:tests/nms_bench/nms.rs
结构:#[cfg(vx_kernel)] 模块放两个 #[no_mangle] pub extern "C" fn
kernel(设备构建时只编这部分);host 构建(默认)通过
vxk!(name) proc-macro 生成 name!(<<<g,b>>>() 的 chevron 启动器,
FFI 与启动助手复用 tests/rust_compat/common.rs。
rust
//! NMS (non-maximum suppression) on Vortex --- the **Rust** twin of
//! `tests/cuda_compat/nms.cu` (the C side, stock CUDA C built through the
//! same `vxcc` CUDA-compat flow). Same two-stage algorithm, same test
//! data, same CPU reference:
//!
//! build_masks --- SIMT-parallel over box i: lane i records which
//! higher-scored boxes j < i overlap it (IoU > thr)
//! as bits of a u32 bitmask row (W = ceil(N/32)).
//! resolve_keep --- one lane walks the rows and resolves the final
//! keep mask (box i is kept iff no kept higher-scored
//! box suppressed it).
//!
//! ci/vxrustc -o nms_rust nms.rs && ./nms_rust [keep.hex]
//!
//! C twin: cd tests/cuda_compat && vxcc -o nms nms.cu <rt>/libvortex.so
#![allow(dead_code)]
#[macro_use]
#[path = "../rust_compat/common.rs"]
mod common;
// chevron launcher: `kernel!(<<<grid, block>>>(args)` --- proc-macro (host
// build only; the device build is cfg-gated away from it)
#[cfg(not(vx_kernel))]
use vxk_macro::vxk;
#[cfg(not(vx_kernel))]
vxk!(build_masks);
#[cfg(not(vx_kernel))]
vxk!(resolve_keep);
// ── shared algorithm (device kernels + host reference) ────────────────────
/// IoU of two (x1,y1,x2,y2) boxes --- the Rust spelling of nms.cu's
/// `dev_iou`: fmaxf/fminf → `f32::max`/`f32::min` (both maximumNumber,
/// the same `fmax.s`/`fmin.s` the backend picks for the C side).
/// `#[inline(always)]` mirrors `__forceinline__`.
#[inline(always)]
fn dev_iou(ax1: f32, ay1: f32, ax2: f32, ay2: f32,
bx1: f32, by1: f32, bx2: f32, by2: f32) -> f32 {
let xx1 = ax1.max(bx1);
let yy1 = ay1.max(by1);
let xx2 = ax2.min(bx2);
let yy2 = ay2.min(by2);
let w = (xx2 - xx1).max(0.0);
let h = (yy2 - yy1).max(0.0);
let inter = w * h;
let area_a = (ax2 - ax1) * (ay2 - ay1);
let area_b = (bx2 - bx1) * (by2 - by1);
let uni = area_a + area_b - inter;
if uni > 0.0 { inter / uni } else { 0.0 }
}
#[cfg(vx_kernel)]
pub mod kx {
use crate::common::device::*;
use crate::dev_iou;
/// nms.cu's build_masks_kernel. boxes arrive pre-sorted by score
/// desc; row i of `masks` (W u32 words) gets bit j set when box j
/// (j < i) overlaps box i above `iou_thr`.
#[no_mangle]
pub extern "C" fn build_masks(boxes: *const f32, n: i32, w: i32,
iou_thr: f32, masks: *mut u32) {
unsafe {
let tid = vxr_thread_id_x();
let bid = vxr_block_id_x();
let bdim = vxr_block_dim_x();
let i = bid * bdim + tid;
if i >= n as u32 {
return;
}
let row = masks.add(i as usize * w as usize);
for u in 0..(w as usize) {
*row.add(u) = 0; // clear the row (no memset)
}
let ib = 4 * i as usize;
let (ax1, ay1, ax2, ay2) =
(*boxes.add(ib), *boxes.add(ib + 1),
*boxes.add(ib + 2), *boxes.add(ib + 3));
let mut j = 0u32;
while j < i {
let jb = 4 * j as usize;
let (bx1, by1, bx2, by2) =
(*boxes.add(jb), *boxes.add(jb + 1),
*boxes.add(jb + 2), *boxes.add(jb + 3));
if dev_iou(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
> iou_thr
{
*row.add((j >> 5) as usize) |= 1u32 << (j & 31);
}
j += 1;
}
}
}
/// nms.cu's resolve_keep_kernel: one lane walks the bitmask rows.
#[no_mangle]
pub extern "C" fn resolve_keep(masks: *const u32, n: i32, w: i32,
keep: *mut u8) {
unsafe {
let tid = vxr_thread_id_x();
let bid = vxr_block_id_x();
if bid != 0 || tid != 0 {
return;
}
let mut i = 0u32;
while i < n as u32 {
let row = masks.add(i as usize * w as usize);
let mut suppressed = false;
let mut j = 0u32;
while j < i {
if *keep.add(j as usize) == 0 {
j += 1;
continue; // suppressed suppresses none
}
if *row.add((j >> 5) as usize) & (1u32 << (j & 31)) != 0 {
suppressed = true;
break;
}
j += 1;
}
*keep.add(i as usize) = if suppressed { 0 } else { 1 };
i += 1;
}
}
}
}
// ── host ──────────────────────────────────────────────────────────────────
#[cfg(not(vx_kernel))]
mod host_nms {
pub const N: usize = 6;
pub const THR: f32 = 0.5;
pub const NT: u32 = 32;
pub const W: usize = (N + 31) / 32;
// nms.cu's test set (x1, y1, x2, y2, score)
pub const IN: [(f32, f32, f32, f32, f32); N] = [
(100.0, 100.0, 210.0, 210.0, 0.90),
(105.0, 105.0, 215.0, 215.0, 0.85),
(102.0, 102.0, 212.0, 212.0, 0.80),
(300.0, 300.0, 400.0, 400.0, 0.70),
(302.0, 302.0, 402.0, 402.0, 0.65),
(500.0, 500.0, 600.0, 600.0, 0.95),
];
/// score-desc order (nms.cu: std::sort with strict `>`; the test set
/// has no ties, so the order is unique).
pub fn order_desc() -> Vec<usize> {
let mut order: Vec<usize> = (0..N).collect();
order.sort_by(|&a, &b| IN[b].4.partial_cmp(&IN[a].4).unwrap());
order
}
/// nms.cu's nms_cpu (greedy reference), Rust spelling. keep[a] = 1
/// when original-index box `a` survives.
pub fn cpu_ref(order: &[usize]) -> [u8; N] {
let mut removed = [false; N];
let mut keep = [0u8; N];
for (i, &a) in order.iter().enumerate() {
if removed[a] {
continue;
}
keep[a] = 1;
for &b in &order[i + 1..] {
if !removed[b]
&& crate::dev_iou(IN[a].0, IN[a].1, IN[a].2, IN[a].3,
IN[b].0, IN[b].1, IN[b].2, IN[b].3)
> THR
{
removed[b] = true;
}
}
}
keep
}
}
#[cfg(not(vx_kernel))]
fn main() {
use common::ffi;
use common::host;
use host_nms::*;
let hex_path = std::env::args().nth(1);
let order = order_desc();
// sorted-by-score boxes, as nms.cu uploads them
let mut boxes = vec![0f32; 4 * N];
for (i, &a) in order.iter().enumerate() {
boxes[4 * i + 0] = IN[a].0;
boxes[4 * i + 1] = IN[a].1;
boxes[4 * i + 2] = IN[a].2;
boxes[4 * i + 3] = IN[a].3;
}
let ref_keep = cpu_ref(&order);
let bb = host::buffer(ffi::VX_MEM_READ | ffi::VX_MEM_WRITE,
(4 * N) as u64 * 4);
let mb = host::buffer(ffi::VX_MEM_READ | ffi::VX_MEM_WRITE,
N as u64 * W as u64 * 4);
let kb = host::buffer(ffi::VX_MEM_READ | ffi::VX_MEM_WRITE, N as u64);
let (ab, mb_a, kb_a) = (host::addr(bb), host::addr(mb), host::addr(kb));
host::wait(host::upload(bb, host::bytemsg(&boxes)));
let n: i32 = N as i32;
let w: i32 = W as i32;
let block = (NT, 1, 1);
// launches are program-ordered on the queue; only the final event
// needs gating the readback
let _ev1 = build_masks!(<<<(1u32, 1, 1), block>>>(
ab as *const f32, n, w, THR, mb_a as *mut u32));
let ev2 = resolve_keep!(<<<(1u32, 1, 1), block>>>(
mb_a as *const u32, n, w, kb_a as *mut u8));
// u8 readback gated on the resolve_keep launch (common::host::download
// speaks f32 slices; the keep mask is bytes)
let mut keep = vec![0u8; N];
{
let mut e: ffi::Event = std::ptr::null_mut();
unsafe {
host::check(ffi::vx_enqueue_read(
host::queue(),
keep.as_mut_ptr() as *mut std::os::raw::c_void,
kb, 0, N as u64, 1, &ev2, &mut e),
"vx_enqueue_read");
}
host::wait(e);
}
// device keep (sorted order) vs CPU ref (original order)
let mut errors = 0;
for i in 0..N {
let dev_kept = keep[i];
let ref_kept = ref_keep[order[i]];
if dev_kept != ref_kept {
errors += 1;
eprintln!("box {} (orig {}): device keep={} ref keep={}",
i, order[i], dev_kept, ref_kept);
}
}
// report like nms.cu: kept ORIGINAL indices, highest score first
let kept: Vec<usize> =
(0..N).filter(|&i| keep[i] == 1).map(|i| order[i]).collect();
println!("IoU threshold = {:.2}", THR);
print!("keep ({}):", kept.len());
for &k in &kept {
print!(" {}", k);
}
println!();
if let Some(p) = &hex_path {
let mut s = String::new();
for b in &keep {
s.push_str(&format!("{:02x}", b));
}
std::fs::write(p, s + "\n").ok();
}
if errors != 0 {
println!("FAILED!");
std::process::exit(1);
}
println!("PASSED!");
host::release(bb);
host::release(mb);
host::release(kb);
}
kernel 与 nms.cu 的逐行对应关系:
| nms.cu | nms.rs |
|---|---|
__device__ __forceinline__ dev_iou |
#[inline(always)] fn dev_iou(同模块可见,设备/host 构建都用到) |
blockIdx.x*blockDim.x+threadIdx.x |
vxr_block_id_x() * vxr_block_dim_x() + vxr_thread_id_x()(extern "C",设备端由 shim 提供) |
for (w) row[w]=0 |
for u in 0..w { *row.add(u)=0 }(语义相同;IR 形态不同,见 §4.2) |
| `row[j>>5] | = 1u << (j&31)` |
build_masks_kernel<<<blocks,256>>> |
build_masks!(<<<(1,1,1),(32,1,1)>>>()(N=6 时 blocks=1、block 取 32 即可,i>=n 守卫兜底) |
cudaMalloc/Memcpy/getLastError/DeviceSynchronize |
common::host 的 buffer/upload/wait/event(同一 C API) |
std::sort 按 score 降序 |
sort_by(partial_cmp) 同序(测试集无平局,序唯一) |
3. 编译与复现
3.1 环境
bash
export PATH=/data/vortex/llvm_vortex_release/bin:$PATH
# 工具链库必须前置!继承来的旧 LD_LIBRARY_PATH(如
# /home/ubuntu/tools/llvm-vortex/lib)会让 fork 的 clang 加载错 libLLVM,
# 报 "unknown argument: -fvortex-kernel-only"(vxc 在自己的子进程里也做了同样防护)
export LD_LIBRARY_PATH=/data/vortex/llvm_vortex_release/lib:/data/vortex/build/sw/runtime:$LD_LIBRARY_PATH
export VORTEX_DRIVER=simx
3.2 C 侧:nms.cu 的编译方法
一条命令出单 ELF(kernel 镜像内嵌,VXSYMTAB 节):
bash
vxc -o out/nms tests/cuda_compat/nms.cu build/sw/runtime/libvortex.so
只要 kernel 镜像(供反汇编对比):
bash
vxc --device-only -o out/dev_c tests/cuda_compat/nms.cu # → dev_c/kernel.elf + kernel.vxbin
要拿到 C 侧设备 IR ,做法是复现 vxc 设备 pass 的完整 argv
(用 VXCC_VERBOSE=1 vxc --device-only ... 可打印),只把输出形式
换成 -emit-llvm -S(-fvortex-kernel-only 在 codegen 阶段丢弃 host
函数体,所以模块里恰好只剩 kernel 本体):
bash
XCONFIGS=$(python3 ci/gen_config.py --config=VX_config.toml --cflags=-DVX_CFG_XLEN=64)
clang++ --target=riscv64-unknown-elf \
--sysroot=$TC/riscv64-unknown-elf --gcc-toolchain=$TC \
-Xclang -target-feature -Xclang +xvortex \
-Xclang -target-feature -Xclang +zicond \
-mllvm -disable-loop-idiom-all -march=rv64imafd -mabi=lp64d \
-std=c++17 -fvortex-launch -fvortex-kernel-only -O3 -fno-rtti \
-fno-exceptions -fdata-sections -ffunction-sections \
-I sw/kernel/include -I build/sw -I build/hw \
-I sw/runtime/include -I sw/common \
-DNDEBUG -D__VORTEX__ $XCONFIGS \
-mllvm -vortex-divergence-max-bbs=1024 \
-x c++ tests/cuda_compat/nms.cu -emit-llvm -S -o out/ir/nms_c_device.ll
要点:+xvortex 开启 Vortex ISA 扩展;-fvortex-launch 处理 chevron
改写;-fvortex-kernel-only 只出设备函数;
-mllvm -vortex-divergence-max-bbs=1024 是分歧控制流 pass 的阈值
(缺了它行为不同)。设备链接阶段产物 kernel.elf → 转换 → kernel.vxbin。
3.3 Rust 侧:nms.rs 的编译方法(ci/vxrustc 管线)
nms.rs ──rustc --cfg vx_kernel --crate-type=lib -O --emit=llvm-ir──▶ d.ll (x86_64 目标,纯 target-neutral IR)
d.ll ──ci/vxmark.py --kernels build_masks,resolve_keep──▶ d_marked.ll (vortex.kernel 注解+retain+llvm.used+设备 triple)
d_marked.ll + sw/rust/vx_rust_shim.c ──vxc --device-only──▶ kernel.elf → kernel.vxbin
kernel.vxbin ──生成 C blob(__vx_detail::__vx_kernel_blob / _size 符号)──▶ blob.o
nms.rs ──rustc(host,--extern vxk_macro=so, link-args="shim_host.o blob.o -lvortex")──▶ nms_rust (单 ELF)
等价的一键展开:
bash
rustc --cfg vx_kernel --crate-type=lib --edition 2021 -O \
--emit=llvm-ir nms.rs -o out/ir/nms_rust_device.ll
python3 ci/vxmark.py out/ir/nms_rust_device.ll out/ir/nms_rust_marked.ll \
--kernels build_masks,resolve_keep --triple riscv64-unknown-unknown-elf
vxc --device-only -o out/dev_rust out/ir/nms_rust_marked.ll sw/rust/vx_rust_shim.c
ci/vxrustc -o out/nms_rust nms.rs # 上面三步 + blob + host 链接,一条命令
设计原因(rustc 侧约束):本机 rustc 1.75 的 sysroot 只有 x86_64 std,
没有 riscv 设备 rlib ⇒ 设备 IR 必须用 x86_64 目标 编译并保持
target-neutral(kernel 体只用算术/访存/extern "C" 调用,禁用 asm! 与
x86 std::arch);--cfg vx_kernel 把 host 部分(含 fn main)裁掉,
--crate-type=lib 规避 no-main 的 bin 报错。vxmark 负责把 rustc 的 IR
"认成" Vortex kernel:补 @llvm.global.annotations("vortex.kernel")、
@llvm.used、"vortex-kernel" 属性,并把 triple 换成设备 triple
(后端对"哪个函数是 kernel"的识别是 IR 层的,与前端语言无关)。
3.4 一键复现:tests/nms_bench/run.sh
bash
cd tests/nms_bench && ./run.sh
步骤:① C 侧单 ELF + 设备镜像;② C 侧设备 IR;③ Rust 侧
设备 IR → vxmark → 设备 pass → 单 ELF;④ 两个 kernel.elf 反汇编;
⑤ 运行两侧,自检(各对 CPU 参考)+ 交叉核对(keep 列表必须一致)。
产物:
out/
├── nms # C 单 ELF
├── nms_rust # Rust 单 ELF
├── dev_c/kernel.{elf,vxbin} # C 设备镜像
├── dev_rust/kernel.{elf,vxbin} # Rust 设备镜像
├── ir/nms_c_device.ll # C 设备 IR
├── ir/nms_rust_device.ll # Rust 设备 IR(rustc 原始输出)
├── ir/nms_rust_marked.ll # 经 vxmark 标记后
├── asm/asm_c.txt # llvm-objdump -d dev_c/kernel.elf
├── asm/asm_rust.txt # llvm-objdump -d dev_rust/kernel.elf
└── keep_rust.hex # 010100000100
4. LLVM IR 对比
三份子文件都在 out/ir/。模块级差异总表:
| 维度 | C(nms_c_device.ll) |
Rust(nms_rust_device.ll / nms_rust_marked.ll) |
|---|---|---|
| target triple | riscv64-unknown-unknown-elf(设备 pass 直接以设备 target 编译) |
原始 x86_64-unknown-linux-gnu;vxmark 剥掉 triple/datalayout 后打 riscv64-unknown-unknown-elf |
| 符号名 | Itanium mangled:_Z18build_masks_kernelPKfiifPj、_Z7dev_iouffffffff |
#[no_mangle]:裸名 build_masks、resolve_keep |
| 函数数量 | 3 个 define:2 kernel + dev_iou(linkonce_odr ... comdat) |
2 个 define:dev_iou 被 #[inline(always)] 完全内联,无独立函数 |
| CSR 读取 | call i64 asm sideeffect "csrr $0, $1", "=r,i"(i32 3286) + trunc + call @llvm.riscv.vx.uniform.i32.i32 |
tail call @vxr_thread_id_x() 等 extern 声明(设备端 shim 实现) |
| 局部状态 | alloca + llvm.lifetime.start/end + llvm.var.annotation(uniform) |
无(rustc 寄存器分配,IR 层不落 alloca) |
i >= N 比较 |
icmp slt(有符号,int 语义) |
icmp ult(无符号,u32 语义) |
| 行清零 | 显式 store 循环(IR 层可看到 loop header/phi) | tail call @llvm.memset.p0.i64(rustc 把 for u in 0..w { *row.add(u)=0 } 模式识别为 memset) |
| 基本块 | 编号块(19: ; preds = %5) |
命名块(start:、bb5:、bb7.preheader:、bb11.lr.ph:) |
| 参数属性 | noalias nocapture noundef readonly、signext |
nocapture noundef、unnamed_addr |
| kernel 标记 | clang 自动生成:@llvm.global.annotations("vortex.kernel","vortex.device",真实文件名/行号)+ @llvm.used |
rustc 不生成;vxmark 补 @llvm.global.annotations("vortex.kernel",文件 "rust.rs")、@llvm.used、attributes #990 = { "vortex-kernel" } |
!vortex.kargs 参数 ABI 元数据 |
有 :build_masks 为 !{!"8:0 8:0 8:0 8:0 8:0"},resolve_keep 为 !{!"8:0 8:0 8:0 8:0"} |
无(raw-IR 源不携带)------直接导致 §5.1 的机器码差异 |
| max/min 固有函数 | llvm.maxnum.f32/llvm.minnum.f32 调用 6 处(在 build_masks,dev_iou 内联)+ comdat 函数体 6 处 |
llvm.maxnum.f32/llvm.minnum.f32 调用 6 处(build_masks,dev_iou 内联),无独立函数 |
fmaxf/fminf 与 f32::max/f32::min 都编译成 maximumNumber/minimumNumber
语义,IR 层就收敛到同一对固有函数------这是后文热循环逐指令一致的伏笔。
4.1 CSR 读取:内联 asm vs extern 调用
C 侧(nms.cu 的 blockIdx.x 经 __UNIFORM__ 宏展开):
llvm
; Function Attrs: mustprogress nounwind
define dso_local void @_Z18build_masks_kernelPKfiifPj(ptr noalias nocapture noundef readonly %0,
i32 noundef signext %1, i32 noundef signext %2, float noundef %3,
ptr noalias nocapture noundef %4) #0 !vortex.kargs !6 {
%7 = alloca i32, align 4
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %7) #6
call void @llvm.var.annotation.p0.p0(ptr nonnull %7, ptr nonnull @.str, ptr nonnull @.str.1, i32 65, ptr null)
%8 = call i64 asm sideeffect "csrr $0, $1", "=r,i"(i32 3286) #6, !srcloc !7
%9 = trunc i64 %8 to i32
%10 = call noundef i32 @llvm.riscv.vx.uniform.i32.i32(i32 %9)
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %7) #6
%11 = call i64 asm sideeffect "csrr $0, $1", "=r,i"(i32 3289) #6, !srcloc !8
%13 = call noundef i32 @llvm.riscv.vx.uniform.i32.i32(i32 %12)
%14 = mul i32 %13, %10
%15 = call i64 asm sideeffect "csrr $0, $1", "=r,i"(i32 3283) #6, !srcloc !9
%17 = add i32 %14, %16
%18 = icmp slt i32 %17, %1
br i1 %18, label %19, label %99
3283/3286/3289 = 0xCD3/0xCD6/0xCD9(thread_id_x/block_id_x/block_dim_x)。
每个 CSR 读取 = csrr 内联 asm + trunc + llvm.riscv.vx.uniform
固有函数(uniform 提示,供分歧控制流 pass 消费),外加
lifetime.start/end + var.annotation 把 blockIdx.x 等局部量钉在栈上。
Rust 侧(kernel 体第一行):
llvm
define void @build_masks(ptr nocapture noundef readonly %boxes, i32 noundef %n,
i32 noundef %w, float noundef %iou_thr, ptr nocapture noundef %masks)
unnamed_addr #0 {
start:
%tid = tail call noundef i32 @vxr_thread_id_x()
%bid = tail call noundef i32 @vxr_block_id_x()
%bdim = tail call noundef i32 @vxr_block_dim_x()
%_10 = mul i32 %bdim, %bid
%i = add i32 %_10, %tid
%_11.not = icmp ult i32 %i, %n
br i1 %_11.not, label %bb5, label %bb17
vxr_thread_id_x 等是 declare(extern),设备链接时由
sw/rust/vx_rust_shim.c 的 C 实现满足(其本体就是 csrr + 符号扩展)。
同一件事,两种 IR 形状:C 侧"内联 + uniform 固有函数",
Rust 侧"外部调用"。机器码后果见 §5.2(内联 csrr vs jal stub)。
4.2 行清零:store 循环 vs memset 固有函数
C 侧 for (w) row[w] = 0u 在 IR 层就是一个普通循环
(带 icmp sgt 空循环消除的 zicond 形态):
llvm
%24 = icmp sgt i32 %2, 0 ; W > 0 ?
br i1 %24, label %25, label %27
...
%50: ; loop header
%51 = phi i64 [ 0, %25 ], [ %53, %50 ]
... ; store i32 0, row + %51
Rust 侧同一个语义(for u in 0..(w as usize) { *row.add(u) = 0 })
被 rustc 模式识别为 memset:
llvm
bb7.preheader:
%0 = shl nuw nsw i64 %_16, 2
tail call void @llvm.memset.p0.i64(ptr align 4 %row, i8 0, i64 %0, i1 false)
br label %bb9
这条差异一路传导到机器码:C 侧是内联的带谓词 store 循环
(sw zero + vx_pred_n 循环),Rust 侧是 jal 调用一个被拖进
kernel 镜像的完整 memset 实现 (bare-metal libc,见 §5.4)。
这是两个前端在"同一语义"上最可见的 IR 形状差。
4.3 dev_iou:comdat 独立定义 vs 纯内联
C 侧 __device__ __forceinline__ 的 IR 结果是"既内联又留了 comdat 本体 ":
build_masks 内有 6 处 llvm.maxnum/minnum.f32(内联体),同时模块里还有一份
linkonce_odr ... comdat 的 _Z7dev_iouffffffff(6 处同样的调用)------
后者因带 vortex.device 注解在设备 IR 中存活,但链接阶段
--gc-sections 把它裁掉(最终 kernel.elf 符号表里没有它)。
Rust 侧 #[inline(always)] 则是只内联、无本体 :全模块 6 处
max/min 调用全在 build_masks 里(resolve_keep 不用 IoU,为 0 处)。
两侧"实际进机器码的 IoU 代码"完全等价。
4.4 !vortex.kargs:参数 ABI 的自描述元数据(最本质的差异)
C 侧设备 pass 给每个 kernel 记了参数 ABI 表:
llvm
define dso_local void @_Z18build_masks_kernelPKfiifPj(...) #0 !vortex.kargs !6 {
...
!6 = !{!"8:0 8:0 8:0 8:0 8:0"} ; 5 个参数 × 每参 8 字节槽,byval=0
!19 = !{!"8:0 8:0 8:0 8:0"} ; resolve_keep:4 参数 × 8 字节槽
格式是 "size:byval" 每 IR 参数一项(size 必为 8 的倍数)。后端解包 pass
(RISCVVortexKernelArgUnpack.cpp 的 parseKArgsTable)读到该表后,
参数偏移 = 各槽 size 的累加和 ⇒ 8 字节槽布局 。这张表与 host 侧
CodeGen 生成的启动 stub 严格配套:EmitVortexKernelHostStubBody
(CodeGenModule.cpp)把每个参数按"每 ABI 寄存器一个 8 字节零初始化槽"
打包进 thread-local blob------host 怎么打包,IR 里就写明设备怎么解包,
ABI 自描述、端到端自洽。
Rust 侧的 IR 是 raw-IR 源(.ll 直接喂给设备 pass),不带这张表 ⇒
解包 pass 走 legacy 路径 packedOffsets(F, DL):按每个 IR 参数的
ABI 对齐 逐个排布(ptr 对齐 8、i32/f32 对齐 4)⇒
偏移 0, 8, 12, 16, 24。Rust host 端的打包器(common.rs 的
pack_arg,规则 off = align_up(off, alignof(T)))与此一致。
⇒ 两条流各自的 (host 打包, IR 元数据, 设备解包) 三段全部自洽 ,
只是布局规则不同(§5.1 的机器码差异由此而来)。
4.5 vxmark:Rust IR 如何被"认成" Vortex kernel
nms_rust_marked.ll 相对 rustc 原始输出的改动(尾部可见):
llvm
target triple = "riscv64-unknown-unknown-elf" ; 覆盖 x86_64 triple,剥掉 x86 datalayout
...
attributes #990 = { "vortex-kernel" } ; kernel 属性(后端 RISCVRegisterInfo 认)
@.str.vortexkernel = private unnamed_addr constant [14 x i8] c"vortex.kernel\00", section "llvm.metadata"
@llvm.global.annotations = appending global [2 x { ptr, ptr, ptr, i32, ptr }]
[{ ... { ptr @build_masks, ptr @.str.vortexkernel, ptr @.str.vortexfile, i32 1, ptr null },
... { ptr @resolve_keep, ptr @.str.vortexkernel, ptr @.str.vortexfile, i32 1, ptr null }],
section "llvm.metadata"
@llvm.used = appending global [2 x ptr] [ptr @build_masks, ptr @resolve_keep]
C 侧这份标注是 clang 在 __global__ 展开时自动生成的(内容相同,
另多一份 dev_iou 的 "vortex.device" 条目和真实文件名/行号)。
后端机器 pass 只认 @llvm.global.annotations 里字符串为
"vortex.kernel" 的函数------与前端语言无关,这正是"零 LLVM 改动支持
Rust kernel"的依据。
5. Vortex 汇编(机器指令)对比
反汇编来自 llvm-objdump -d 两个 kernel.elf(全文在 out/asm/)。
符号对照(llvm-nm):
| C 镜像 | Rust 镜像 |
|---|---|
_Z18build_masks_kernelPKfiifPj.vortex.body |
build_masks.vortex.body |
_Z18build_masks_kernelPKfiifPj(解包 wrapper)+ W __vx_kentry_... |
build_masks(wrapper)+ W __vx_kentry_build_masks |
_Z19resolve_keep_kernelPKjiiPh[.vortex.body] |
resolve_keep[.vortex.body] |
| --- | vxr_thread_id_x / vxr_block_id_x / vxr_block_dim_x(shim stub) |
| --- | memset(bare-metal libc 完整实现) |
两侧的 kernel 都被拆成"解包 wrapper(以 kernel 名命名,单参数 = kargs blob
指针)+ 原函数体(.vortex.body 后缀,逐字节不动)+ __vx_kentry_* 弱别名
(打包工具据此枚举入口)"------结构完全相同,差异在细节。
5.1 kentry 参数解包:8B 槽位 vs 原生对齐
两个 kernel 的参数都是 (ptr, i32, i32, f32/...)。kentry wrapper 负责把
kargs blob 里的各参数加载回 ABI 寄存器:
asm
; C:build_masks_kernel kentry(__vx_kentry__Z18build_masks_kernelPKfiifPj)
1800002b0: addi sp, sp, -0x10
1800002b4: sd ra, 0x8(sp)
1800002b8: ld a4, 0x0(a0) ; boxes @0
1800002bc: lw a1, 0x8(a0) ; N @8
1800002c0: lw a2, 0x10(a0) ; W @0x10
1800002c4: flw fa0, 0x18(a0) ; iou_thr @0x18
1800002c8: ld a3, 0x20(a0) ; masks @0x20
1800002cc: mv a0, a4
1800002d0: jal ... <_Z18build_masks_kernelPKfiifPj.vortex.body>
1800002d4: ld ra, 0x8(sp)
1800002d8: addi sp, sp, 0x10
1800002dc: ret
; Rust:build_masks kentry(__vx_kentry_build_masks)
1800002e0: addi sp, sp, -0x10
1800002e4: sd ra, 0x8(sp)
1800002e8: ld a4, 0x0(a0) ; boxes @0
1800002ec: lw a1, 0x8(a0) ; n @8
1800002f0: lw a2, 0xc(a0) ; w @0xc ← 对齐 4,不占满 8B 槽
1800002f4: flw fa0, 0x10(a0) ; iou_thr @0x10 ← 紧跟其后
1800002f8: ld a3, 0x18(a0) ; masks @0x18 ← 对齐 8
1800002fc: mv a0, a4
180000300: jal ... <build_masks.vortex.body>
180000304: ld ra, 0x8(sp)
180000308: addi sp, sp, 0x10
18000030c: ret
wrapper 骨架(存 ra → 按偏移取参 → mv a0,a4 → jal 本体 → 恢复)
完全相同 ;唯一的差别是偏移:C 侧每参固定 8B 槽
(0,8,0x10,0x18,0x20,来自 !vortex.kargs 表),
Rust 侧原生对齐(0,8,0xc,0x10,0x18,来自 legacy packedOffsets)。
resolve_keep 同理:C 0/8/0x10/0x18;Rust 0/8/0xc/0x10。
两个镜像各自的 host 打包方(vxc CodeGen stub / Rust pack_arg)
按同一规则写 blob,所以各自端到端正确。
5.2 CSR 访问:体内内联 csrr vs jal shim stub
C 侧函数体开头直接读 CSR(因为 IR 里就是内联 asm):
asm
0000000180000034 <_Z18build_masks_kernelPKfiifPj.vortex.body>:
180000034: csrr a4, 0xcd6 ; block_id_x
180000038: csrr a5, 0xcd9 ; block_dim_x
18000003c: csrr a6, 0xcd3 ; thread_id_x
180000040: mul a4, a5, a4
180000044: addw a4, a4, a6
180000048: slt a5, a4, a1
18000004c: vx_split_n a1, a5
180000050: beqz a5, 0x1800001b4 ; i>=N 的 lane 先走 join
Rust 侧函数体开头是三次 jal 调用,跳到镜像内的 shim stub:
asm
0000000180000034 <build_masks.vortex.body>:
...
180000050: jal 0x18000033c <vxr_thread_id_x>
...
180000058: jal 0x180000348 <vxr_block_id_x>
...
180000060: jal 0x18000035c <vxr_block_dim_x>
...
000000018000033c <vxr_thread_id_x>:
18000033c: csrr a0, 0xcd3
180000340: sext.w a0, a0
180000344: ret
0000000180000348 <vxr_block_id_x>:
180000348: addi sp, sp, -0x10
18000034c: csrr a0, 0xcd6
180000350: sext.w a0, a0
180000354: addi sp, sp, 0x10
180000358: ret
000000018000035c <vxr_block_dim_x>:
18000035c: addi sp, sp, -0x10
180000360: csrr a0, 0xcd9
180000364: sext.w a0, a0
180000368: addi sp, sp, 0x10
18000036c: ret
连锁后果:Rust 函数体因为含调用(jal),带标准 prologue/epilogue
(sd ra + s 寄存器保存);C 函数体零调用、无寄存器 prologue
(.vortex.body 从第一条 csrr 直接开始,sp 帧只在分歧区域内按需建立)。
另外 C 侧 uniform 提示(llvm.riscv.vx.uniform)经 pass 剥离后不留痕迹,
Rust 侧跨 C 调用边界的 uniform 提示天然丢失(纯性能项,不影响正确性)。
5.3 Rust 镜像里多了一个 memset
Rust 侧行清零走 llvm.memset.p0.i64(§4.2)⇒ 机器码是一次 jal:
asm
1800000a8: jal 0x180000370 <memset>
libvortex.a 里 memset 是未定义符号(U),设备链接时由 bare-metal
libc 提供,于是完整的 memset 实现(0x370--0x1548,约 1.2KB)被打进
Rust kernel 镜像 。C 侧对应的是内联 store 循环,镜像里没有 memset
符号------这是 Rust 镜像体积/指令数多于 C 侧的主要来源:
asm
; C 侧行清零(带谓词的 store 循环,循环体仅 4 条)
18000008c: sw zero, 0x0(a7)
180000090: addi a7, a7, 0x4
180000094: xor t0, a7, a2
180000098: seqz t0, t0
18000009c: vx_pred_n t0, a3
1800000a0: beqz t0, 0x18000008c
5.4 IoU 热点循环:逐指令几乎一致
dev_iou 内联后的 j 循环主体------两侧浮点指令序列完全相同
(fmax.s fmax.s fmin.s fmin.s fsub.s×4 fmul.s fmax.s×2 fadd.s fmul.s fsub.s flt.s fdiv.s),只有寄存器分配和基址寄存器不同:
asm
; C:build_masks_kernel 的 IoU 体(每轮 j)
180000118: flw ft1, -0x8(a0) ; bx1..by2
18000011c: flw ft2, -0x4(a0)
180000120: flw ft3, 0x0(a0)
180000124: flw ft4, 0x4(a0)
180000128: fmax.s ft5, fa5, ft1 ; xx1 = max(ax1,bx1)
18000012c: fmax.s ft6, fa4, ft2
180000130: fmin.s ft7, fa3, ft3 ; xx2/yy2
180000134: fmin.s fa6, fa2, ft4
180000138: fsub.s ft1, ft3, ft1
18000013c: fsub.s ft2, ft4, ft2
180000140: fsub.s ft3, ft7, ft5 ; w_raw,h_raw
180000144: fsub.s ft4, fa6, ft6
180000148: fmul.s ft1, ft1, ft2 ; area 乘积
18000014c: fmax.s ft2, ft3, ft0 ; w = max(w_raw, 0.0)(ft0 = +0.0)
180000150: fmax.s ft3, ft4, ft0
180000154: fadd.s ft1, fa1, ft1 ; uni = area_a + inter...
180000158: fmul.s ft2, ft2, ft3 ; inter = w*h
18000015c: fsub.s ft1, ft1, ft2
180000160: flt.s t1, ft0, ft1 ; 0.0 < uni
180000164: fdiv.s ft1, ft2, ft1 ; inter/uni(uni=0 时为 NaN)
180000168: xor t1, t1, zero ; NaN 探测(比较结果 NaN→1)
18000016c: snez t2, t1
180000170: vx_split t1, t2 ; 分歧:NaN 分支
180000174: bnez t2, 0x18000017c
180000178: fmv.s ft1, ft0 ; iou = 0.0
18000017c: vx_join t1
180000180: flt.s t2, fa0, ft1 ; iou > iou_thr ?
180000184: vx_split_n t1, t2
180000188: beqz t2, 0x1800000f8
18000018c: srliw t2, a6, 0x5 ; row[j>>5] |= 1<<(j&31)
180000190: slli t2, t2, 0x2
180000194: add t2, a5, t2
180000198: lw t3, 0x0(t2)
18000019c: sllw t4, t0, a3
1800001a0: or t3, t3, t4
1800001a4: sw t3, 0x0(t2)
1800001a8: j 0x1800000f8
; Rust:build_masks 的 IoU 体(同一段,不同寄存器)
18000012c: flw ft0, -0x8(s1)
180000130: flw ft1, -0x4(s1)
180000134: flw ft2, 0x0(s1)
180000138: flw ft3, 0x4(s1)
18000013c: fmax.s ft4, fa5, ft0
180000140: fmax.s ft5, fa4, ft1
180000144: fmin.s ft6, fa3, ft2
180000148: fmin.s ft7, fa2, ft3
18000014c: fsub.s ft0, ft2, ft0
180000150: fsub.s ft1, ft3, ft1
180000154: fsub.s ft2, ft6, ft4
180000158: fsub.s ft3, ft7, ft5
18000015c: fmul.s ft0, ft0, ft1
180000160: fmax.s ft1, ft2, fa0
180000164: fmax.s ft2, ft3, fa0
180000168: fadd.s ft0, fa1, ft0
18000016c: fmul.s ft1, ft1, ft2
180000170: fsub.s ft0, ft0, ft1
180000174: flt.s a5, fa0, ft0
180000178: fdiv.s ft0, ft1, ft0
18000017c: xor a5, a5, zero
180000180: snez a6, a5
180000184: vx_split a5, a6
180000188: bnez a6, 0x180000190
18000018c: fmv.s ft0, fa0
180000190: vx_join a5
180000194: flt.s a6, fs0, ft0
180000198: vx_split_n a5, a6
18000019c: beqz a6, 0x18000010c
1800001a0: srliw a6, a2, 0x5
1800001a4: slli a6, a6, 0x2
1800001a8: add a6, s0, a6
1800001ac: lw a7, 0x0(a6)
1800001b0: sllw t0, a4, a1
1800001b4: or a7, a7, t0
1800001b8: sw a7, 0x0(a6)
1800001bc: j 0x18000010c
细节对照:
- 两侧都保留了源码的"先除后判零"结构:算出
inter/uni(uni=0 ⇒ NaN),
用flt 0.0, uni的结果做 NaN 探测(xor t,t,zero+snez),
再用vx_split/bnez/fmv.s/vx_join把iou = 0.0分支合并回来------
同样的 NaN-safe select 惯用法。 - 位掩码置位序列
srliw/slli/add/lw/sllw/or/sw两侧逐条对应。 - 循环出口判定两侧同为
xor/seqz/vx_pred_n/bnez
(C:xor t1,a4,a6; seqz; vx_pred_n t1,a7; bnez;
Rust:xor a5,s2,a2; seqz; vx_pred_n a5,a3; bnez)。
5.5 分歧控制流:同一套 Vortex 指令族
两侧的分支都没有变成传统跳指令,而是被分歧控制流 pass
(-mllvm -vortex-divergence-max-bbs=1024)统一改写成 Vortex 的
统一控制流 + 活跃掩码指令:
| 指令 | 作用(本例中的出现) |
|---|---|
vx_split rs1, rs2 |
记录当前 tmask,按 rs2 切出活跃 lane 分支(如 iou>thr 的置位分支、NaN 分支) |
vx_split_n rs1, rs2 |
同上的非零/否定形态(如 i>=n 的提前退出、iou>thr 判定) |
vx_pred rs1, rs2 / vx_pred_n rs1, rs2 |
循环内按 lane 谓词继续/退出(行清零循环、j 循环出口) |
vx_join rs1 |
汇合,恢复 tmask |
tmask CSR |
csrr a,tmask 取当前活跃掩码参与谓词合成 |
这套机制工作在机器指令层(在 IR 差异之后发生),所以两种语言的
同一个源码分支得到完全同形的 vx_* 序列 ------机器码对比里
"看起来一样"的部分,恰恰是 Vortex ISA 的核心。
6. 运行结果
$ ./out/nms # C(nms.cu on Vortex)
IoU threshold = 0.50
GPU keep (3): 5 0 3
CPU keep (3): 5 0 3
✓ 结果一致
$ ./out/nms_rust keep_rust.hex # Rust(nms.rs on Vortex)
IoU threshold = 0.50
keep (3): 5 0 3
PASSED!
$ cat keep_rust.hex
010100000100
- keep 字节(排序序:5,0,1,2,3,4)=
01 01 00 01 00 00:
box 5 与 box 0 保留、box 3 保留,box 1/2 被 box 0 抑制、box 4 被 box 3 抑制。 - 两侧各自对内置 CPU 贪心参考自检通过;
run.sh再做跨语言交叉核对:两边 keep 列表(原始下标)必须一致------5 0 3。 - C 侧 block=256、Rust 侧 block=32(N=6,
i>=n守卫使多余 lane 空转),
不影响结果,仅说明两种启动路径都通。
7. 结论
- 前端差异在 IR 层充分可见且合理 :内联
csrrasm + uniform 固有函数
vsvxr_*extern 调用;store 循环 vs memset 固有函数;icmp sltvs
icmp ult;mangled vsno_mangle;comdat 本体 vs 纯内联;
以及最关键的!vortex.kargs自描述参数 ABI 表(C 有,Rust raw-IR 没有)。 - 后端以 IR 元数据为契约,机器码热路径收敛 :kentry 包装/参数解包/
分歧控制流改写都发生在 IR 之后,依据的是vortex.kernel注解与
kargs 表(或缺表时的 legacy 规则)------因此两种语言得到的
vx_split/vx_join/vx_pred序列和 IoU 浮点指令序列几乎逐指令相同。 - 参数 ABI 有两套布局,各自端到端自洽 :C/CUDA 流 = host CodeGen stub
的 8B 槽打包 +!vortex.kargs表 + 按槽解包;Rust 流 = hostpack_arg
的 alignof 打包 + 无表 legacypackedOffsets+ 按原生对齐解包。
kernel 的参数偏移永远可以从它自己的 IR(元数据或缺省)推出,
不存在跨流混配的空间。 - Rust 侧的固有代价 :镜像里多 3 条 shim stub 和 ~1.2KB 的
memset,
函数体多一个 prologue;uniform 提示跨 C 调用边界丢失(性能项)。
收益是零 LLVM 改动、零 vxcc C++ 改动、零运行时改动------
整条 Rust 管线只是rustc → vxmark → 现有 vxc --device-only。 - 对"kernel 用什么语言写"这个问题,本例给出的答案是:
语言只决定你看到的 IR 和镜像里的边角料,热循环由后端唯一决定。
附录:复现清单
bash
cd /data/vortex/tests/nms_bench
./run.sh # 一键:两侧构建 + IR/汇编导出 + 运行 + 交叉核对
# 产物见 out/{nms,nms_rust,dev_c,dev_rust,ir,asm,keep_rust.hex}
# IR 全文: out/ir/nms_c_device.ll, out/ir/nms_rust_device.ll, out/ir/nms_rust_marked.ll
# 汇编全文: out/asm/asm_c.txt, out/asm/asm_rust.txt
相关代码:
- C 侧:
tests/cuda_compat/nms.cu(原样使用) - Rust 侧:
tests/nms_bench/nms.rs+tests/rust_compat/common.rs(FFI/启动) - 管线:
ci/vxmark.py(IR 标记)、ci/vxrustc(一键)、sw/rust/vx_rust_shim.c(设备 intrinsics) - 解包/打包机制:
llvm_vortex/llvm/lib/Target/RISCV/RISCVVortexKernelArgUnpack.cpp
(parseKArgsTable/packedOffsets)、
llvm_vortex/clang/lib/CodeGen/CodeGenModule.cpp(EmitVortexKernelHostStubBody、
!vortex.kargs表生成)