同一个 NMS 算子,两种语言实现:CUDA C 与 Rust 生成 LLVM IR 和 Vortex 汇编的差异

同一个 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 访问方式(内联 csrr vs
    jal 到 3 条 stub)和 Rust 镜像里多拖进来一个 memset 的差别。
  • 结论:语言差异在前端(源码→IR)就被"翻译"成了两种不同的 IR 形状,
    但 Vortex 后端(机器 pass:kentry 包装、参数解包、分歧控制流改写)
    以 IR 元数据为契约 工作,因此最终机器码的热路径几乎相同------
    后端才是唯一的事实来源,语言只是入口。

1. 算法与测试数据

NMS(non-maximum suppression)采用两段式 SIMT 实现:

  1. 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))。
  2. 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. 结论

  1. 前端差异在 IR 层充分可见且合理 :内联 csrr asm + uniform 固有函数
    vs vxr_* extern 调用;store 循环 vs memset 固有函数;icmp slt vs
    icmp ult;mangled vs no_mangle;comdat 本体 vs 纯内联;
    以及最关键的 !vortex.kargs 自描述参数 ABI 表(C 有,Rust raw-IR 没有)。
  2. 后端以 IR 元数据为契约,机器码热路径收敛 :kentry 包装/参数解包/
    分歧控制流改写都发生在 IR 之后,依据的是 vortex.kernel 注解与
    kargs 表(或缺表时的 legacy 规则)------因此两种语言得到的
    vx_split/vx_join/vx_pred 序列和 IoU 浮点指令序列几乎逐指令相同。
  3. 参数 ABI 有两套布局,各自端到端自洽 :C/CUDA 流 = host CodeGen stub
    的 8B 槽打包 + !vortex.kargs 表 + 按槽解包;Rust 流 = host pack_arg
    的 alignof 打包 + 无表 legacy packedOffsets + 按原生对齐解包。
    kernel 的参数偏移永远可以从它自己的 IR(元数据或缺省)推出,
    不存在跨流混配的空间。
  4. Rust 侧的固有代价 :镜像里多 3 条 shim stub 和 ~1.2KB 的 memset,
    函数体多一个 prologue;uniform 提示跨 C 调用边界丢失(性能项)。
    收益是零 LLVM 改动、零 vxcc C++ 改动、零运行时改动------
    整条 Rust 管线只是 rustc → vxmark → 现有 vxc --device-only。
  5. 对"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 表生成)
相关推荐
滕州市燕猫虎计算机科技工作室个体工商户1 小时前
《Rust程序设计》学习笔记一
开发语言·rust·学习笔记·rust程序设计
lhbweilai1 小时前
深挖FreeRTOS内核实现(二)
c语言·汇编·stm32·单片机
杨丰玮4182 小时前
C语言核心语法
java·c语言·开发语言·学习方法
Zwawa2 小时前
反馈:让系统知道自己做得对不对
c语言·单片机·嵌入式
燕落南枝2 小时前
c语言 洛谷P1765手机题解
c语言·开发语言
白色的北极熊3 小时前
c语言 求余运算符
c语言·数据结构·算法
小溪学编程3 小时前
C语言篇:枚举类型
java·c语言·前端
小溪学编程3 小时前
C语言篇:语言结构
c语言·开发语言·算法
老王爱玩车4 小时前
字符串和字符串函数
c语言·开发语言·数据结构·学习