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 向量化与分块策略。
浙公网安备 33010602011771号