AIGC标识 RISC-V多核并行推理实战:计算图划分、核间同步与缓存一致性调优

1. 从单核加宽到多核协同

前文讨论过向量位宽(VLEN)加宽对单核吞吐的提升,但其收益伴随寄存器堆面积与功耗的线性增长,宽度超过一定阈值后功耗约束成为瓶颈。多核是正交的扩展维度:多个 hart 各自持有独立向量部件,通过共享内存协作。代价是通信——任务划分、同步、缓存一致性由此成为工程主体。

多核推理的并行收益来自三个层次:

  • 批次维:多个独立请求分配到不同核,互不通信,实现最简单;
  • 算子内维:单个大算子(GEMM、Softmax)沿输出行或归约维切分,各核产出部分和后归并;
  • 算子间维:相邻算子按流水线错开执行,掩盖访存延迟。

层次越深,同步点越多。推理是周期性执行的静态负载,与操作系统调度不同,其同步开销可通过离线分析预估并纳入执行计划。

2. 任务划分的三种模式

数据并行将请求或序列沿 batch 维切分。各核执行同一份代码与同一份只读权重,无写共享,一致性流量几乎为零。缺点是 batch=1 的场景(对话式生成的 decode 阶段)无从切分。

模型并行在算子内部切分。GEMM 沿输出行划分是 embarrassingly parallel 的经典形态:每核持有一份权重分块,输出互不相交;沿归约维(K 维)切分则需部分和归并,引入一次显式同步。工程上优先沿输出维切,归并交给下一算子自然完成。

流水线并行将计算图按层划分为多段,各段驻留一个核,中间激活沿流水线传递。它掩盖访存延迟,但深流水在 batch=1 时每周期只有一个核满载,且段间需要背压机制防止中间张量堆积。

划分粒度由两个量共同决定:单任务执行时间与同步开销之比(过细则同步占比失控),以及各核 L1 私有容量(过细则工作集碎片化)。实践起点是"每核单次任务不小于一次完整算子调用",再按 profiling 数据向下细分。

3. 核间同步原语

RISC-V 的 RVWMO 内存模型允许加载与存储乱序生效。经典的 flag-data 发布模式——生产者写数据、置标志,消费者轮询标志、读数据——若无屏障,消费者可能在标志置位前观察到旧数据,或以乱序读到数据的新旧混合。正确写法是在置标志前执行 fence rw, w(先写后发布),消费者在判定标志后执行 fence r, rw(先获取后读)。

计数与锁原语由原子操作承担:

  • amoadd:引用计数、完成计数。各核对同一地址做原子加,全部完成后计数值等于核数,即可越过后栅栏;
  • amoswap/amoxor:自旋锁的加锁与解锁。LR/SC(lr.w/sc.w)对可实现无 ABA 的锁,但在高争用下自旋失败率上升;AMO 由一致性协议保证原子性,行为更可预期;
  • ticket 锁:用两次 amoadd 实现 FIFO 公平性,避免核数增多时自旋锁吞吐退化。

同步原语的选择依据是争用强度:低争用用简单自旋锁;高争用、临界区极短的场景,直接用原子计数替代互斥;需要公平性时上 ticket 锁。所有原语的共同底线是——临界区不含任何阻塞调用(分配内存、I/O),否则自旋等待会将浪费放大 N 倍。

4. 缓存一致流量与伪共享

多核协作的隐性成本在一致性协议层面。以 MOESI 类协议为例:只读权重在每个核的 L1 中均处于共享干净态,读取不产生一致性流量,这是数据并行模式"天然便宜"的机制根源;而任一核写共享地址会使其他核的副本失效,触发失效-重取往返。

伪共享是最典型的浪费:各核本无逻辑共享,但各自频繁写入的计数器恰好落在同一缓存行内,硬件一致性机制将它们强行串行化。症状是多核加速比在核数增加后不升反降,且核间失效计数异常升高。修复方式是按缓存行边界填充(padding),将每核私有写热数据隔开到不同行。RISC-V 平台缓存行常见为 64 字节,具体数值以处理器手册为准。

在真实多核 SoC 上验证划分策略时,簇内一致性互联的带宽与延迟直接决定归并类同步的成本上限。以玄铁 C950 为例,其通过 XT-Link 一致性互联可组成最高 8 核 Cluster(详见 玄铁 C950 产品页)。在硬件一致性簇内,原子操作经由缓存一致性协议在缓存层级完成仲裁,无需每轮同步都穿透主存——这一结构性差异决定了高频短临界区同步原语(如逐层完成计数)在簇内与跨簇间的成本相差可达数量级;具体仲裁层级与往返时延以处理器文档为准。

5. 实战:四核数据并行 + 部分和归并

以下示例演示"沿输出行切分 GEMV、amoadd 完成计数、主核归并"的最小并行骨架,使用 C11 原子接口(GCC 对 RISC-V 映射到 AMO 指令序列):

#include <stdatomic.h>
#include <pthread.h>

#define NCORE 4
#define N     1024          /* 输出维度 */
#define K     4096          /* 归约维度 */

float w[N][K] __attribute__((aligned(64)));
float x[K]    __attribute__((aligned(64)));
float y[N]    __attribute__((aligned(64)));

atomic_int done;                       /* 完成计数 */

void *worker(void *arg)
{
    long id = (long)arg;
    /* 按 64B 对齐切分,避免相邻核写同一缓存行(伪共享防护) */
    int lo = (int)(N / NCORE / 16 * 16) * (int)id;
    int hi = lo + (int)(N / NCORE / 16 * 16);

    for (int i = lo; i < hi; i++) {
        float acc = 0.0f;
        for (int k = 0; k < K; k++)
            acc += w[i][k] * x[k];
        y[i] = acc;
    }
    atomic_fetch_add_explicit(&done, 1, memory_order_acq_rel);
    return NULL;
}

float run_parallel(void)
{
    pthread_t th[NCORE];
    atomic_store(&done, 0);
    for (long i = 1; i < NCORE; i++)
        pthread_create(&th[i], NULL, worker, (void *)i);
    worker((void *)0);
    for (int i = 1; i < NCORE; i++)
        pthread_join(th[i], NULL);
    /* done == NCORE,全部分块完成 */
    return y[0];
}

三处细节值得展开:

  • 切分按 16 个 float(64 字节)对齐:保证相邻核写入的输出区间不共享缓存行,直接消除伪共享;
  • memory_order_acq_rel:fetch_add 的获取语义使本核此前对 y 的普通写对该原子操作之后的读取方可见,释放语义使读取 done 的核先看到数据再看到计数——对应第 3 节的 fence 语义,编译器会生成成对的 fence 指令;
  • 主核参与计算而非纯等待,消除一个核的空转。

验证与观测方法:以 perf stat(或 cycles、instructions 计数)对比单核与四核版本耗时得到加速比;观察 perf 的缓存一致相关事件(若平台暴露)或以注释掉 aligned(64) 的版本对照,可量化伪共享的实际代价。加速比数据依赖具体处理器的核数、一致性互联与缓存层级配置,不可由单一平台的测量外推为普遍规律。

6. 常见问题与成因

  • 加速比饱和:同步点占比随核数增长。定位方法是分别测量纯计算时间与含同步的墙钟时间,二者差值即同步开销;优化方向是增大任务粒度或改用无归并的划分维;
  • 核数越多越慢:优先排查伪共享(第 4 节)与高争用自旋锁,两者的症状都是失效流量随核数超线性增长;
  • 偶发结果错误:缺 fence 的发布模式。RVWMO 下错误是概率性的,复现困难,排查时应审计所有跨核 flag-data 序列是否具备获取-释放配对;
  • 负载不均:划分均匀但核间频率或缓存状态不同。可改用任务队列 + work stealing,以动态平衡抵消静态差异,代价是引入队列同步点。

7. 结语

本文完成多核并行推理的三个工程环节:以划分模式匹配负载形态,以 AMO 与 fence 构建正确的同步原语,以缓存行为单位管理一致性流量。下一篇转向推理之外的运行时基础设施——动态链接与延迟绑定在 RISC-V 上的实现(ELF PLT/GOT、__tls_get_addr 与向量 ABI 的交互),继续补齐端侧软件栈的底层机制。


本文代码在 RV64GC 目标上以 -O2 -pthread 编译通过;示例为教学骨架,生产实现应叠加前文讨论的 RVV 向量化与分块策略。

posted @ 2026-09-15 18:17  RISCV_Explore  阅读(12)  评论(0)    收藏  举报